From 2409c1a2fbb3423b7d972ee5fa23f36262cf21aa Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 11 Sep 2023 17:31:00 +1000 Subject: [PATCH 001/393] A POC to show how ExecutionResult wrapping could be avoided --- .../AbstractAsyncExecutionStrategy.java | 9 +- .../execution/AsyncExecutionStrategy.java | 23 +- .../AsyncSerialExecutionStrategy.java | 2 +- .../graphql/execution/ExecutionStrategy.java | 222 ++++++++++++------ .../graphql/execution/FieldValueInfo.java | 19 +- .../SubscriptionExecutionStrategy.java | 3 +- .../ChainedInstrumentation.java | 74 +++++- .../ExecuteObjectInstrumentationContext.java | 45 ++++ .../instrumentation/Instrumentation.java | 68 +++++- .../NoContextChainedInstrumentation.java | 18 +- .../SimplePerformantInstrumentation.java | 20 ++ ...teObjectInstrumentationContextAdapter.java | 49 ++++ ...onResultInstrumentationContextAdapter.java | 35 +++ .../DataLoaderDispatcherInstrumentation.java | 12 + .../FieldLevelTrackingApproach.java | 43 ++++ .../BreadthFirstExecutionTestStrategy.java | 4 +- .../execution/BreadthFirstTestStrategy.java | 4 +- .../execution/ExecutionStrategyTest.groovy | 7 +- .../execution/FieldValueInfoTest.groovy | 2 +- .../ChainedInstrumentationStateTest.groovy | 12 +- .../InstrumentationTest.groovy | 6 +- .../ModernTestingInstrumentation.groovy | 24 ++ .../NamedInstrumentation.groovy | 6 + ...NoContextChainedInstrumentationTest.groovy | 2 +- ...ExecuteObjectInstrumentationContext.groovy | 9 + 25 files changed, 590 insertions(+), 128 deletions(-) create mode 100644 src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java create mode 100644 src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java create mode 100644 src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java create mode 100644 src/test/groovy/graphql/execution/instrumentation/TestingExecuteObjectInstrumentationContext.groovy diff --git a/src/main/java/graphql/execution/AbstractAsyncExecutionStrategy.java b/src/main/java/graphql/execution/AbstractAsyncExecutionStrategy.java index 577bb00f96..863e0d6fad 100644 --- a/src/main/java/graphql/execution/AbstractAsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AbstractAsyncExecutionStrategy.java @@ -22,18 +22,17 @@ public AbstractAsyncExecutionStrategy(DataFetcherExceptionHandler dataFetcherExc super(dataFetcherExceptionHandler); } - // This method is kept for backward compatibility. Prefer calling/overriding another handleResults method - protected BiConsumer, Throwable> handleResults(ExecutionContext executionContext, List fieldNames, CompletableFuture overallResult) { - return (List results, Throwable exception) -> { + protected BiConsumer, Throwable> handleResults(ExecutionContext executionContext, List fieldNames, CompletableFuture overallResult) { + return (List results, Throwable exception) -> { if (exception != null) { handleNonNullException(executionContext, overallResult, exception); return; } Map resolvedValuesByField = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size()); int ix = 0; - for (ExecutionResult executionResult : results) { + for (Object result : results) { String fieldName = fieldNames.get(ix++); - resolvedValuesByField.put(fieldName, executionResult.getData()); + resolvedValuesByField.put(fieldName, result); } overallResult.complete(new ExecutionResultImpl(resolvedValuesByField, executionContext.getErrors())); }; diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 6608fba0f3..11d7239c3e 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -42,35 +42,24 @@ public CompletableFuture execute(ExecutionContext executionCont ExecutionStrategyInstrumentationContext executionStrategyCtx = ExecutionStrategyInstrumentationContext.nonNullCtx(instrumentation.beginExecutionStrategy(instrumentationParameters, executionContext.getInstrumentationState())); - MergedSelectionSet fields = parameters.getFields(); - List fieldNames = fields.getKeys(); - Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size()); - for (String fieldName : fieldNames) { - MergedField currentField = fields.getSubField(fieldName); - - ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); - ExecutionStrategyParameters newParameters = parameters - .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); - - CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); - futures.add(future); - } + List fieldNames = parameters.getFields().getKeys(); + Async.CombinedBuilder futures = getAsyncFieldValueInfo(executionContext, parameters); CompletableFuture overallResult = new CompletableFuture<>(); executionStrategyCtx.onDispatched(overallResult); futures.await().whenComplete((completeValueInfos, throwable) -> { - BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldNames, overallResult); + BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldNames, overallResult); if (throwable != null) { handleResultsConsumer.accept(null, throwable.getCause()); return; } - Async.CombinedBuilder executionResultFutures = Async.ofExpectedSize(completeValueInfos.size()); + Async.CombinedBuilder fieldValuesFutures = Async.ofExpectedSize(completeValueInfos.size()); for (FieldValueInfo completeValueInfo : completeValueInfos) { - executionResultFutures.add(completeValueInfo.getFieldValue()); + fieldValuesFutures.add(completeValueInfo.getFieldValueFuture()); } executionStrategyCtx.onFieldValuesInfo(completeValueInfos); - executionResultFutures.await().whenComplete(handleResultsConsumer); + fieldValuesFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index fc2dde0980..1c0e4b3924 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -39,7 +39,7 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); ImmutableList fieldNames = ImmutableList.copyOf(fields.keySet()); - CompletableFuture> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, prevResults) -> { + CompletableFuture> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, prevResults) -> { MergedField currentField = fields.getSubField(fieldName); ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); ExecutionStrategyParameters newParameters = parameters diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 4ed0f1e644..2bf82ee7ce 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -1,6 +1,7 @@ package graphql.execution; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.GraphQLError; @@ -15,6 +16,8 @@ import graphql.execution.directives.QueryDirectivesImpl; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters; @@ -41,6 +44,7 @@ import graphql.schema.LightDataFetcher; import graphql.util.FpKit; import graphql.util.LogKit; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +55,7 @@ import java.util.OptionalInt; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -131,9 +136,8 @@ public abstract class ExecutionStrategy { protected final FieldCollector fieldCollector = new FieldCollector(); protected final ExecutionStepInfoFactory executionStepInfoFactory = new ExecutionStepInfoFactory(); - private final ResolveType resolvedType = new ResolveType(); - protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; + private final ResolveType resolvedType = new ResolveType(); /** * The default execution strategy constructor uses the {@link SimpleDataFetcherExceptionHandler} @@ -153,6 +157,22 @@ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHand this.dataFetcherExceptionHandler = dataFetcherExceptionHandler; } + @Internal + public static String mkNameForPath(Field currentField) { + return mkNameForPath(Collections.singletonList(currentField)); + } + + @Internal + public static String mkNameForPath(MergedField mergedField) { + return mkNameForPath(mergedField.getFields()); + } + + @Internal + public static String mkNameForPath(List currentField) { + Field field = currentField.get(0); + return field.getResultKey(); + } + /** * This is the entry point to an execution strategy. It will be passed the fields to execute and get values for. * @@ -161,28 +181,110 @@ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHand * * @return a promise to an {@link ExecutionResult} * - * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value + * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ public abstract CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException; + /** + * This is the re-entry point for an execution strategy when an object type needs to be resolved. + * + * @param executionContext contains the top level execution parameters + * @param parameters contains the parameters holding the fields to be executed and source object + * + * @return a promise to an {@link Map} + * + * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value + */ + protected CompletableFuture> executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + Instrumentation instrumentation = executionContext.getInstrumentation(); + InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); + + ExecuteObjectInstrumentationContext resolveObjectCtx = ExecuteObjectInstrumentationContext.nonNullCtx( + instrumentation.beginExecuteObject(instrumentationParameters, executionContext.getInstrumentationState()) + ); + + List fieldNames = parameters.getFields().getKeys(); + Async.CombinedBuilder resolvedFieldFutures = getAsyncFieldValueInfo(executionContext, parameters); + CompletableFuture> overallResult = new CompletableFuture<>(); + resolveObjectCtx.onDispatched(overallResult); + + resolvedFieldFutures.await().whenComplete((completeValueInfos, throwable) -> { + BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldNames, overallResult); + if (throwable != null) { + handleResultsConsumer.accept(null, throwable); + return; + } + + Async.CombinedBuilder resultFutures = Async.ofExpectedSize(completeValueInfos.size()); + for (FieldValueInfo completeValueInfo : completeValueInfos) { + resultFutures.add(completeValueInfo.getFieldValueFuture()); + } + resolveObjectCtx.onFieldValuesInfo(completeValueInfos); + resultFutures.await().whenComplete(handleResultsConsumer); + }).exceptionally((ex) -> { + // if there are any issues with combining/handling the field results, + // complete the future at all costs and bubble up any thrown exception so + // the execution does not hang. + resolveObjectCtx.onFieldValuesException(); + overallResult.completeExceptionally(ex); + return null; + }); + + overallResult.whenComplete(resolveObjectCtx::onCompleted); + return overallResult; + } + + private BiConsumer, Throwable> buildFieldValueMap(List fieldNames, CompletableFuture> overallResult) { + return (List results, Throwable exception) -> { + if (exception != null) { + handleValueException(overallResult, exception); + return; + } + Map resolvedValuesByField = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size()); + int ix = 0; + for (Object fieldValue : results) { + String fieldName = fieldNames.get(ix++); + resolvedValuesByField.put(fieldName, fieldValue); + } + overallResult.complete(resolvedValuesByField); + }; + } + + @NotNull + Async.CombinedBuilder getAsyncFieldValueInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + MergedSelectionSet fields = parameters.getFields(); + Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size()); + for (String fieldName : fields.getKeys()) { + MergedField currentField = fields.getSubField(fieldName); + + ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); + ExecutionStrategyParameters newParameters = parameters + .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); + + CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); + futures.add(future); + } + return futures; + } + /** * Called to fetch a value for a field and resolve it further in terms of the graphql query. This will call - * #fetchField followed by #completeField and the completed {@link ExecutionResult} is returned. + * #fetchField followed by #completeField and the completed Object is returned. *

* An execution strategy can iterate the fields to be executed and call this method for each one *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it - * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. + * in the query, hence the fieldList. However, the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * - * @return a promise to an {@link ExecutionResult} + * @return a promise to an {@link Object} * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ - protected CompletableFuture resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - return resolveFieldWithInfo(executionContext, parameters).thenCompose(FieldValueInfo::getFieldValue); + protected CompletableFuture resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + return resolveFieldWithInfo(executionContext, parameters).thenCompose(FieldValueInfo::getFieldValueFuture); } /** @@ -206,7 +308,7 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationContext fieldCtx = nonNullCtx(instrumentation.beginField( + InstrumentationContext fieldCtx = nonNullCtx(instrumentation.beginFieldExecution( new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() )); @@ -214,10 +316,10 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> completeField(executionContext, parameters, fetchedValue)); - CompletableFuture executionResultFuture = result.thenCompose(FieldValueInfo::getFieldValue); + CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); - fieldCtx.onDispatched(executionResultFuture); - executionResultFuture.whenComplete(fieldCtx::onCompleted); + fieldCtx.onDispatched(fieldValueFuture); + fieldValueFuture.whenComplete(fieldCtx::onCompleted); return result; } @@ -415,7 +517,7 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, fetchedValue); - InstrumentationContext ctxCompleteField = nonNullCtx(instrumentation.beginFieldComplete( + InstrumentationContext ctxCompleteField = nonNullCtx(instrumentation.beginFieldCompletion( instrumentationParams, executionContext.getInstrumentationState() )); @@ -434,13 +536,12 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters); - CompletableFuture executionResultFuture = fieldValueInfo.getFieldValue(); + CompletableFuture executionResultFuture = fieldValueInfo.getFieldValueFuture(); ctxCompleteField.onDispatched(executionResultFuture); executionResultFuture.whenComplete(ctxCompleteField::onCompleted); return fieldValueInfo; } - /** * Called to complete a value for a field based on the type of the field. *

@@ -461,10 +562,10 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); Object result = executionContext.getValueUnboxer().unbox(parameters.getSource()); GraphQLType fieldType = executionStepInfo.getUnwrappedNonNullType(); - CompletableFuture fieldValue; + CompletableFuture fieldValue; if (result == null) { - return getFieldValueInfoForNull(executionContext, parameters); + return getFieldValueInfoForNull(parameters); } else if (isList(fieldType)) { return completeValueForList(executionContext, parameters, result); } else if (isScalar(fieldType)) { @@ -481,12 +582,13 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut GraphQLObjectType resolvedObjectType; try { resolvedObjectType = resolveType(executionContext, parameters, fieldType); - fieldValue = completeValueForObject(executionContext, parameters, resolvedObjectType, result); + CompletableFuture> objectValue = completeValueForObject(executionContext, parameters, resolvedObjectType, result); + fieldValue = objectValue.thenApply(map -> map); } catch (UnresolvedTypeException ex) { // consider the result to be null and add the error on the context handleUnresolvedTypeProblem(executionContext, parameters, ex); // complete field as null, validating it is nullable - return getFieldValueInfoForNull(executionContext, parameters); + return getFieldValueInfoForNull(parameters); } return FieldValueInfo.newFieldValueInfo(OBJECT).fieldValue(fieldValue).build(); } @@ -501,22 +603,21 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra /** * Called to complete a null value. * - * @param executionContext contains the top level execution parameters - * @param parameters contains the parameters holding the fields to be executed and source object + * @param parameters contains the parameters holding the fields to be executed and source object * * @return a {@link FieldValueInfo} * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ - private FieldValueInfo getFieldValueInfoForNull(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - CompletableFuture fieldValue = completeValueForNull(executionContext, parameters); + private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters parameters) { + CompletableFuture fieldValue = completeValueForNull(parameters); return FieldValueInfo.newFieldValueInfo(NULL).fieldValue(fieldValue).build(); } - protected CompletableFuture completeValueForNull(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + protected CompletableFuture completeValueForNull(ExecutionStrategyParameters parameters) { return Async.tryCatch(() -> { Object nullValue = parameters.getNonNullFieldValidator().validate(parameters.getPath(), null); - return completedFuture(new ExecutionResultImpl(nullValue, executionContext.getErrors())); + return completedFuture(nullValue); }); } @@ -538,7 +639,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(exceptionallyCompletedFuture(e)).build(); } if (resultIterable == null) { - return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(completedFuture(new ExecutionResultImpl(null, executionContext.getErrors()))).build(); + return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(completedFuture(null)).build(); } return completeValueForList(executionContext, parameters, resultIterable); } @@ -561,7 +662,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, iterableValues); Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationContext completeListCtx = nonNullCtx(instrumentation.beginFieldListComplete( + InstrumentationContext completeListCtx = nonNullCtx(instrumentation.beginFieldListCompletion( instrumentationParams, executionContext.getInstrumentationState() )); @@ -587,25 +688,21 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, index++; } - CompletableFuture> resultsFuture = Async.each(fieldValueInfos, FieldValueInfo::getFieldValue); + CompletableFuture> resultsFuture = Async.each(fieldValueInfos, FieldValueInfo::getFieldValueFuture); - CompletableFuture overallResult = new CompletableFuture<>(); + CompletableFuture overallResult = new CompletableFuture<>(); completeListCtx.onDispatched(overallResult); + overallResult.whenComplete(completeListCtx::onCompleted); resultsFuture.whenComplete((results, exception) -> { if (exception != null) { - ExecutionResult executionResult = handleNonNullException(executionContext, overallResult, exception); - completeListCtx.onCompleted(executionResult, exception); + handleValueException(overallResult, exception); return; } List completedResults = new ArrayList<>(results.size()); - for (ExecutionResult completedValue : results) { - completedResults.add(completedValue.getData()); - } - ExecutionResultImpl executionResult = new ExecutionResultImpl(completedResults, executionContext.getErrors()); - overallResult.complete(executionResult); + completedResults.addAll(results); + overallResult.complete(completedResults); }); - overallResult.whenComplete(completeListCtx::onCompleted); return FieldValueInfo.newFieldValueInfo(LIST) .fieldValue(overallResult) @@ -613,6 +710,22 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, .build(); } + protected void handleValueException(CompletableFuture overallResult, Throwable e) { + Throwable underlyingException = e; + if (e instanceof CompletionException) { + underlyingException = e.getCause(); + } + if (underlyingException instanceof NonNullableFieldWasNullException) { + assertNonNullFieldPrecondition((NonNullableFieldWasNullException) underlyingException, overallResult); + if (!overallResult.isDone()) { + overallResult.complete(null); + } + } else { + overallResult.completeExceptionally(e); + } + } + + /** * Called to turn an object into a scalar value according to the {@link GraphQLScalarType} by asking that scalar type to coerce the object * into a valid value @@ -624,7 +737,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, * * @return a promise to an {@link ExecutionResult} */ - protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { + protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { Object serialized; try { serialized = scalarType.getCoercing().serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); @@ -637,7 +750,7 @@ protected CompletableFuture completeValueForScalar(ExecutionCon } catch (NonNullableFieldWasNullException e) { return exceptionallyCompletedFuture(e); } - return completedFuture(new ExecutionResultImpl(serialized, executionContext.getErrors())); + return completedFuture(serialized); } /** @@ -650,7 +763,7 @@ protected CompletableFuture completeValueForScalar(ExecutionCon * * @return a promise to an {@link ExecutionResult} */ - protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { + protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { Object serialized; try { serialized = enumType.serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); @@ -662,7 +775,7 @@ protected CompletableFuture completeValueForEnum(ExecutionConte } catch (NonNullableFieldWasNullException e) { return exceptionallyCompletedFuture(e); } - return completedFuture(new ExecutionResultImpl(serialized, executionContext.getErrors())); + return completedFuture(serialized); } /** @@ -675,7 +788,7 @@ protected CompletableFuture completeValueForEnum(ExecutionConte * * @return a promise to an {@link ExecutionResult} */ - protected CompletableFuture completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { + protected CompletableFuture> completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); FieldCollectorParameters collectorParameters = newParameters() @@ -698,8 +811,7 @@ protected CompletableFuture completeValueForObject(ExecutionCon ); // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. - - return executionContext.getQueryStrategy().execute(executionContext, newParameters); + return executionContext.getQueryStrategy().executeObject(executionContext, newParameters); } @SuppressWarnings("SameReturnValue") @@ -720,7 +832,6 @@ protected GraphQLObjectType resolveType(ExecutionContext executionContext, Execu return resolvedType.resolveType(executionContext, parameters.getField(), parameters.getSource(), parameters.getExecutionStepInfo(), fieldType, parameters.getLocalContext()); } - protected Iterable toIterable(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) { if (FpKit.isIterable(result)) { return FpKit.toIterable(result); @@ -730,14 +841,12 @@ protected Iterable toIterable(ExecutionContext context, ExecutionStrateg return null; } - private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) { TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getExecutionStepInfo().getUnwrappedNonNullType()); logNotSafe.warn("{} got {}", error.getMessage(), result.getClass()); context.addError(error); } - /** * Called to discover the field definition give the current parameters and the AST {@link Field} * @@ -816,7 +925,6 @@ protected ExecutionResult handleNonNullException(ExecutionContext executionConte return executionResult; } - /** * Builds the type info hierarchy for the current field * @@ -862,22 +970,4 @@ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionCo .arguments(argumentValues) .build(); } - - - @Internal - public static String mkNameForPath(Field currentField) { - return mkNameForPath(Collections.singletonList(currentField)); - } - - @Internal - public static String mkNameForPath(MergedField mergedField) { - return mkNameForPath(mergedField.getFields()); - } - - - @Internal - public static String mkNameForPath(List currentField) { - Field field = currentField.get(0); - return field.getResultKey(); - } } diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index 168ffab735..ff0f7817c1 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -1,6 +1,8 @@ package graphql.execution; +import graphql.DeprecatedAt; import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; import graphql.PublicApi; import java.util.ArrayList; @@ -22,10 +24,10 @@ public enum CompleteValueType { } private final CompleteValueType completeValueType; - private final CompletableFuture fieldValue; + private final CompletableFuture fieldValue; private final List fieldValueInfos; - private FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { + private FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValue = fieldValue; @@ -36,7 +38,12 @@ public CompleteValueType getCompleteValueType() { return completeValueType; } + @Deprecated + @DeprecatedAt("2023-09-11") public CompletableFuture getFieldValue() { + return fieldValue.thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()); + } + public CompletableFuture getFieldValueFuture() { return fieldValue; } @@ -60,7 +67,7 @@ public String toString() { @SuppressWarnings("unused") public static class Builder { private CompleteValueType completeValueType; - private CompletableFuture executionResultFuture; + private CompletableFuture fieldValueFuture; private List listInfos = new ArrayList<>(); public Builder(CompleteValueType completeValueType) { @@ -72,8 +79,8 @@ public Builder completeValueType(CompleteValueType completeValueType) { return this; } - public Builder fieldValue(CompletableFuture executionResultFuture) { - this.executionResultFuture = executionResultFuture; + public Builder fieldValue(CompletableFuture executionResultFuture) { + this.fieldValueFuture = executionResultFuture; return this; } @@ -84,7 +91,7 @@ public Builder fieldValueInfos(List listInfos) { } public FieldValueInfo build() { - return new FieldValueInfo(completeValueType, executionResultFuture, listInfos); + return new FieldValueInfo(completeValueType, fieldValueFuture, listInfos); } } } \ No newline at end of file diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index be816e7add..87f5d88ab8 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -135,7 +135,8 @@ private CompletableFuture executeSubscriptionEvent(ExecutionCon FetchedValue fetchedValue = unboxPossibleDataFetcherResult(newExecutionContext, parameters, eventPayload); FieldValueInfo fieldValueInfo = completeField(newExecutionContext, newParameters, fetchedValue); CompletableFuture overallResult = fieldValueInfo - .getFieldValue() + .getFieldValueFuture() + .thenApply(val -> new ExecutionResultImpl(val, executionContext.getErrors())) .thenApply(executionResult -> wrapWithRootFieldName(newParameters, executionResult)); // dispatch instrumentation so they can know about each subscription event diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 70e7bd063f..d1f3121fa5 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -175,6 +175,21 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument return new ChainedExecutionStrategyInstrumentationContext(mapAndDropNulls(instrumentations, mapper)); } + @Override + public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return ExecuteObjectInstrumentationContext.NOOP; + } + Function mapper = instrumentation -> { + InstrumentationState specificState = getSpecificState(instrumentation, state); + return instrumentation.beginExecuteObject(parameters, specificState); + }; + if (instrumentations.size() == 1) { + return mapper.apply(instrumentations.get(0)); + } + return new ChainedExecuteObjectInstrumentationContext(mapAndDropNulls(instrumentations, mapper)); + } + @Override @NotNull public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { @@ -197,9 +212,14 @@ public InstrumentationContext beginField(InstrumentationFieldPa @Override public InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { + return Assert.assertShouldNeverHappen("The deprecated " + "beginField" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginField(parameters, specificState); + return instrumentation.beginFieldExecution(parameters, specificState); }); } @@ -226,12 +246,18 @@ public InstrumentationContext beginFieldComplete(Instrumentatio @Override public InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldComplete" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginFieldComplete(parameters, specificState); + return instrumentation.beginFieldCompletion(parameters, specificState); }); } + @Override @NotNull public InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { @@ -246,6 +272,14 @@ public InstrumentationContext beginFieldListComplete(Instrument }); } + @Override + public @Nullable InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return chainedCtx(instrumentation -> { + InstrumentationState specificState = getSpecificState(instrumentation, state); + return instrumentation.beginFieldListCompletion(parameters, specificState); + }); + } + @Override @NotNull public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { @@ -371,10 +405,6 @@ private ChainedInstrumentationState(List instrumentations, List } } - private InstrumentationState getState(Instrumentation instrumentation) { - return instrumentationToStates.get(instrumentation); - } - private static CompletableFuture combineAll(List instrumentations, InstrumentationCreateStateParameters parameters) { Async.CombinedBuilder builder = Async.ofExpectedSize(instrumentations.size()); for (Instrumentation instrumentation : instrumentations) { @@ -384,6 +414,10 @@ private static CompletableFuture combineAll(List new ChainedInstrumentationState(instrumentations, instrumentationStates)); } + + private InstrumentationState getState(Instrumentation instrumentation) { + return instrumentationToStates.get(instrumentation); + } } private static class ChainedInstrumentationContext implements InstrumentationContext { @@ -434,5 +468,33 @@ public void onFieldValuesException() { } } + private static class ChainedExecuteObjectInstrumentationContext implements ExecuteObjectInstrumentationContext { + + private final ImmutableList contexts; + + ChainedExecuteObjectInstrumentationContext(ImmutableList contexts) { + this.contexts = contexts; + } + + @Override + public void onDispatched(CompletableFuture> result) { + contexts.forEach(context -> context.onDispatched(result)); + } + + @Override + public void onCompleted(Map result, Throwable t) { + contexts.forEach(context -> context.onCompleted(result, t)); + } + + @Override + public void onFieldValuesInfo(List fieldValueInfoList) { + contexts.forEach(context -> context.onFieldValuesInfo(fieldValueInfoList)); + } + + @Override + public void onFieldValuesException() { + contexts.forEach(ExecuteObjectInstrumentationContext::onFieldValuesException); + } + } } diff --git a/src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java new file mode 100644 index 0000000000..f78cbdc0b0 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java @@ -0,0 +1,45 @@ +package graphql.execution.instrumentation; + +import graphql.Internal; +import graphql.PublicSpi; +import graphql.execution.FieldValueInfo; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@PublicSpi +public interface ExecuteObjectInstrumentationContext extends InstrumentationContext> { + + @Internal + ExecuteObjectInstrumentationContext NOOP = new ExecuteObjectInstrumentationContext() { + @Override + public void onDispatched(CompletableFuture> result) { + } + + @Override + public void onCompleted(Map result, Throwable t) { + } + }; + + /** + * This creates a no-op {@link InstrumentationContext} if the one pass in is null + * + * @param nullableContext a {@link InstrumentationContext} that can be null + * + * @return a non null {@link InstrumentationContext} that maybe a no-op + */ + @NotNull + @Internal + static ExecuteObjectInstrumentationContext nonNullCtx(ExecuteObjectInstrumentationContext nullableContext) { + return nullableContext == null ? NOOP : nullableContext; + } + + default void onFieldValuesInfo(List fieldValueInfoList) { + } + + default void onFieldValuesException() { + } + +} diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 77c4c6bd83..a2aee90cf4 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -5,6 +5,8 @@ import graphql.ExecutionResult; import graphql.PublicSpi; import graphql.execution.ExecutionContext; +import graphql.execution.instrumentation.adapters.ExecutionResultInstrumentationContextAdapter; +import graphql.execution.instrumentation.adapters.ExecuteObjectInstrumentationContextAdapter; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; @@ -201,7 +203,7 @@ default InstrumentationContext beginExecuteOperation(Instrument /** * This is called each time an {@link graphql.execution.ExecutionStrategy} is invoked, which may be multiple times - * per query as the engine recursively descends down over the query. + * per query as the engine recursively descends over the query. * * @param parameters the parameters to this step * @@ -218,7 +220,7 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen /** * This is called each time an {@link graphql.execution.ExecutionStrategy} is invoked, which may be multiple times - * per query as the engine recursively descends down over the query. + * per query as the engine recursively descends over the query. * * @param parameters the parameters to this step * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} @@ -230,6 +232,20 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen return beginExecutionStrategy(parameters.withNewState(state)); } + /** + * This is called each time an {@link graphql.execution.ExecutionStrategy} object resolution is called, which may be multiple times + * per query as the engine recursively descends over the query. + * + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * + * @return a nullable {@link ExecutionStrategyInstrumentationContext} object that will be called back when the step ends (assuming it's not null) + */ + @Nullable + default ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + return ExecuteObjectInstrumentationContext.NOOP; + } + /** * This is called each time a subscription field produces a new reactive stream event value and it needs to be mapped over via the graphql field subselection. @@ -284,11 +300,27 @@ default InstrumentationContext beginField(InstrumentationFieldP * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ + @Deprecated + @DeprecatedAt("2023-09-11") @Nullable default InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { return beginField(parameters.withNewState(state)); } + /** + * This is called just before a field is resolved into a value. + * + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * + * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + */ + @Nullable + default InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { + InstrumentationContext ic = beginField(parameters, state); + return ic == null ? null : new ExecutionResultInstrumentationContextAdapter(ic); + } + /** * This is called just before a field {@link DataFetcher} is invoked. * @@ -343,11 +375,27 @@ default InstrumentationContext beginFieldComplete(Instrumentati * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ + @Deprecated + @DeprecatedAt("2023-09-11") @Nullable default InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return beginFieldComplete(parameters.withNewState(state)); } + /** + * This is called just before the complete field is started. + * + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * + * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + */ + @Nullable + default InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + InstrumentationContext ic = beginFieldComplete(parameters, state); + return ic == null ? null : new ExecutionResultInstrumentationContextAdapter(ic); + } + /** * This is called just before the complete field list is started. * @@ -372,11 +420,27 @@ default InstrumentationContext beginFieldListComplete(Instrumen * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ + @Deprecated + @DeprecatedAt("2023-09-11") @Nullable default InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return beginFieldListComplete(parameters.withNewState(state)); } + /** + * This is called just before the complete field list is started. + * + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * + * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + */ + @Nullable + default InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + InstrumentationContext ic = beginFieldListComplete(parameters, state); + return ic == null ? null : new ExecutionResultInstrumentationContextAdapter(ic); + } + /** * This is called to instrument a {@link graphql.ExecutionInput} before it is used to parse, validate * and execute a query, allowing you to adjust what query input parameters are used diff --git a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java index 72e0783b0c..1041e9feb6 100644 --- a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java @@ -11,6 +11,7 @@ import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.language.Document; import graphql.validation.ValidationError; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.function.BiConsumer; @@ -80,14 +81,19 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecutionStrategy(parameters, specificState)); } + @Override + public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecuteObject(parameters, specificState)); + } + @Override public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { return runAll(state, (instrumentation, specificState) -> instrumentation.beginSubscribedFieldEvent(parameters, specificState)); } @Override - public InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { - return runAll(state, (instrumentation, specificState) -> instrumentation.beginField(parameters, specificState)); + public @Nullable InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { + return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldExecution(parameters, specificState)); } @Override @@ -96,13 +102,13 @@ public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchP } @Override - public InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldComplete(parameters, specificState)); + public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldCompletion(parameters, specificState)); } @Override - public InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldListComplete(parameters, specificState)); + public @Nullable InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldListCompletion(parameters, specificState)); } // relies on the other methods from ChainedInstrumentation which this does not change diff --git a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java index 8ad5f8eeff..6e6ec887a8 100644 --- a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java @@ -112,6 +112,11 @@ public InstrumentationState createState() { return ExecutionStrategyInstrumentationContext.NOOP; } + @Override + public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + return ExecuteObjectInstrumentationContext.NOOP; + } + @Override public @NotNull InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { return assertShouldNeverHappen("The deprecated " + "beginSubscribedFieldEvent" + " was called"); @@ -132,6 +137,11 @@ public InstrumentationState createState() { return noOp(); } + @Override + public @Nullable InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { + return noOp(); + } + @Override public @NotNull InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { return assertShouldNeverHappen("The deprecated " + "beginFieldFetch" + " was called"); @@ -152,6 +162,11 @@ public InstrumentationState createState() { return noOp(); } + @Override + public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return noOp(); + } + @Override public @NotNull InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { return assertShouldNeverHappen("The deprecated " + "beginFieldListComplete" + " was called"); @@ -162,6 +177,11 @@ public InstrumentationState createState() { return noOp(); } + @Override + public @Nullable InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return noOp(); + } + @Override public @NotNull ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { return assertShouldNeverHappen("The deprecated " + "instrumentExecutionInput" + " was called"); diff --git a/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java b/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java new file mode 100644 index 0000000000..2c42d55906 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java @@ -0,0 +1,49 @@ +package graphql.execution.instrumentation.adapters; + +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.Internal; +import graphql.execution.FieldValueInfo; +import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * A class to help adapt old {@link ExecutionResult} based ExecutionStrategyInstrumentationContext + * from the newer {@link Map} based ones. + */ +@Internal +public class ExecuteObjectInstrumentationContextAdapter implements ExecuteObjectInstrumentationContext { + + private final ExecutionStrategyInstrumentationContext delegate; + + public ExecuteObjectInstrumentationContextAdapter(ExecutionStrategyInstrumentationContext delegate) { + this.delegate = delegate; + } + + @Override + public void onDispatched(CompletableFuture> result) { + CompletableFuture future = result.thenApply(r -> ExecutionResultImpl.newExecutionResult().data(r).build()); + delegate.onDispatched(future); + // + // when the mapped future is completed, then call onCompleted on the delegate + future.whenComplete(delegate::onCompleted); + } + + @Override + public void onCompleted(Map result, Throwable t) { + } + + @Override + public void onFieldValuesInfo(List fieldValueInfoList) { + delegate.onFieldValuesInfo(fieldValueInfoList); + } + + @Override + public void onFieldValuesException() { + delegate.onFieldValuesException(); + } +} diff --git a/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java b/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java new file mode 100644 index 0000000000..d67eddb8d0 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java @@ -0,0 +1,35 @@ +package graphql.execution.instrumentation.adapters; + +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.Internal; +import graphql.execution.instrumentation.InstrumentationContext; + +import java.util.concurrent.CompletableFuture; + +/** + * A class to help adapt old {@link ExecutionResult} based InstrumentationContext + * from the newer {@link Object} based ones. + */ +@Internal +public class ExecutionResultInstrumentationContextAdapter implements InstrumentationContext { + + private final InstrumentationContext delegate; + + public ExecutionResultInstrumentationContextAdapter(InstrumentationContext delegate) { + this.delegate = delegate; + } + + @Override + public void onDispatched(CompletableFuture result) { + CompletableFuture future = result.thenApply(obj -> ExecutionResultImpl.newExecutionResult().data(obj).build()); + delegate.onDispatched(future); + // + // when the mapped future is completed, then call onCompleted on the delegate + future.whenComplete(delegate::onCompleted); + } + + @Override + public void onCompleted(Object result, Throwable t) { + } +} diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 87c137e303..6d2ca19b02 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -10,6 +10,7 @@ import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; @@ -134,6 +135,17 @@ private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContex return state.getApproach().beginExecutionStrategy(parameters, state.getState()); } + @Override + public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { + DataLoaderDispatcherInstrumentationState state = ofState(rawState); + // + // if there are no data loaders, there is nothing to do + // + if (state.hasNoDataLoaders()) { + return ExecuteObjectInstrumentationContext.NOOP; + } + return state.getApproach().beginObjectResolution(parameters, state.getState()); + } @Override public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index e6e9a5330a..732582efb5 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -8,6 +8,7 @@ import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import org.dataloader.DataLoaderRegistry; @@ -15,6 +16,7 @@ import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; @@ -160,6 +162,47 @@ public void onFieldValuesException() { }; } + ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { + CallStack callStack = (CallStack) rawState; + ResultPath path = parameters.getExecutionStrategyParameters().getPath(); + int parentLevel = path.getLevel(); + int curLevel = parentLevel + 1; + int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); + synchronized (callStack) { + callStack.increaseExpectedFetchCount(curLevel, fieldCount); + callStack.increaseHappenedStrategyCalls(curLevel); + } + + return new ExecuteObjectInstrumentationContext() { + + @Override + public void onDispatched(CompletableFuture> result) { + } + + @Override + public void onCompleted(Map result, Throwable t) { + } + + @Override + public void onFieldValuesInfo(List fieldValueInfoList) { + boolean dispatchNeeded; + synchronized (callStack) { + dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); + } + if (dispatchNeeded) { + dispatch(); + } + } + + @Override + public void onFieldValuesException() { + synchronized (callStack) { + callStack.increaseHappenedOnFieldValueCalls(curLevel); + } + } + }; + } + // // thread safety : called with synchronised(callStack) // diff --git a/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java b/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java index 5a2bab34c1..5d22041e1b 100644 --- a/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java +++ b/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java @@ -62,8 +62,8 @@ private FetchedValue fetchField(ExecutionContext executionContext, ExecutionStra } private void completeValue(ExecutionContext executionContext, Map results, String fieldName, FetchedValue fetchedValue, ExecutionStrategyParameters newParameters) { - ExecutionResult resolvedResult = completeField(executionContext, newParameters, fetchedValue).getFieldValue().join(); - results.put(fieldName, resolvedResult != null ? resolvedResult.getData() : null); + Object resolvedResult = completeField(executionContext, newParameters, fetchedValue).getFieldValueFuture().join(); + results.put(fieldName, resolvedResult); } } diff --git a/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java b/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java index b5f073e79e..f934904c78 100644 --- a/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java +++ b/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java @@ -63,8 +63,8 @@ private CompletableFuture completeFields(ExecutionContext execu FetchedValue fetchedValue = fetchedValues.get(fieldName); try { - ExecutionResult resolvedResult = completeField(executionContext, newParameters, fetchedValue).getFieldValue().join(); - results.put(fieldName, resolvedResult != null ? resolvedResult.getData() : null); + Object resolvedResult = completeField(executionContext, newParameters, fetchedValue).getFieldValueFuture().join(); + results.put(fieldName, resolvedResult); } catch (NonNullableFieldWasNullException e) { assertNonNullFieldPrecondition(e); results = null; diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy index 412ff3fc65..0f1e17e715 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy @@ -139,7 +139,7 @@ class ExecutionStrategyTest extends Specification { executionStrategy.completeValue(executionContext, parameters) then: - 1 * executionContext.queryStrategy.execute(_, _) + 1 * executionContext.queryStrategy.executeObject(_, _) >> CompletableFuture.completedFuture(null) 0 * executionContext.mutationStrategy.execute(_, _) 0 * executionContext.subscriptionStrategy.execute(_, _) } @@ -681,12 +681,13 @@ class ExecutionStrategyTest extends Specification { Map fetchedValues = [:] @Override - InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + @Override + InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { if (parameters.fetchedValue instanceof FetchedValue) { FetchedValue value = (FetchedValue) parameters.fetchedValue fetchedValues.put(parameters.field.name, value) } - return super.beginFieldComplete(parameters, state) + return super.beginFieldCompletion(parameters, state) } } ExecutionContext instrumentedExecutionContext = executionContextBuilder.instrumentation(instrumentation).build() diff --git a/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy b/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy index 0fe4c30cec..f34666fa22 100644 --- a/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy +++ b/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy @@ -14,7 +14,7 @@ class FieldValueInfoTest extends Specification{ fieldValueInfo.fieldValueInfos == [] as List and: "other fields to be null " - fieldValueInfo.fieldValue == null + fieldValueInfo.fieldValueFuture == null fieldValueInfo.completeValueType == null } diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy index f1812e3955..78a1fe8755 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy @@ -53,7 +53,7 @@ class ChainedInstrumentationStateTest extends Specification { "end:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", @@ -62,7 +62,7 @@ class ChainedInstrumentationStateTest extends Specification { "end:complete-id", "end:field-id", - "end:execution-strategy", + "end:execute-object", "end:complete-hero", "end:field-hero", @@ -139,7 +139,7 @@ class ChainedInstrumentationStateTest extends Specification { "end:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", @@ -148,7 +148,7 @@ class ChainedInstrumentationStateTest extends Specification { "end:complete-id", "end:field-id", - "end:execution-strategy", + "end:execute-object", "end:complete-hero", "end:field-hero", @@ -179,7 +179,7 @@ class ChainedInstrumentationStateTest extends Specification { "end:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", @@ -188,7 +188,7 @@ class ChainedInstrumentationStateTest extends Specification { "end:complete-id", "end:field-id", - "end:execution-strategy", + "end:execute-object", "end:complete-hero", "end:field-hero", diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index 85b274089a..a6c7968cf7 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -313,7 +313,7 @@ class InstrumentationTest extends Specification { "onDispatched:fetch-hero", "end:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", "onDispatched:fetch-id", @@ -323,8 +323,8 @@ class InstrumentationTest extends Specification { "end:complete-id", "onDispatched:field-id", "end:field-id", - "onDispatched:execution-strategy", - "end:execution-strategy", + "onDispatched:execute-object", + "end:execute-object", "onDispatched:complete-hero", "end:complete-hero", "onDispatched:field-hero", diff --git a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy index 81648969f7..0053f595dc 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy @@ -61,6 +61,12 @@ class ModernTestingInstrumentation implements Instrumentation { return new TestingExecutionStrategyInstrumentationContext("execution-strategy", executionList, throwableList, useOnDispatch) } + @Override + ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + assert state == instrumentationState + return new TestingExecuteObjectInstrumentationContext("execute-object", executionList, throwableList, useOnDispatch) + } + @Override InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { assert state == instrumentationState @@ -79,6 +85,12 @@ class ModernTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("field-$parameters.field.name", executionList, throwableList, useOnDispatch) } + @Override + InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { + assert state == instrumentationState + return new TestingInstrumentContext("field-$parameters.field.name", executionList, throwableList, useOnDispatch) + } + @Override InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { assert state == instrumentationState @@ -91,12 +103,24 @@ class ModernTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("complete-$parameters.field.name", executionList, throwableList, useOnDispatch) } + @Override + InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + assert state == instrumentationState + return new TestingInstrumentContext("complete-$parameters.field.name", executionList, throwableList, useOnDispatch) + } + @Override InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { assert state == instrumentationState return new TestingInstrumentContext("complete-list-$parameters.field.name", executionList, throwableList, useOnDispatch) } + @Override + InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + assert state == instrumentationState + return new TestingInstrumentContext("complete-list-$parameters.field.name", executionList, throwableList, useOnDispatch) + } + @Override ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { assert state == instrumentationState diff --git a/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy index 4ed707cfba..42157eaa86 100644 --- a/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy @@ -71,6 +71,12 @@ class NamedInstrumentation extends ModernTestingInstrumentation { return super.beginField(parameters, state) } + @Override + InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { + assertState(state) + return super.beginFieldExecution(parameters, state) + } + @Override InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { assertState(state) diff --git a/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy index a76c1e51d6..981f756633 100644 --- a/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy @@ -42,7 +42,7 @@ class NoContextChainedInstrumentationTest extends Specification { "start:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", diff --git a/src/test/groovy/graphql/execution/instrumentation/TestingExecuteObjectInstrumentationContext.groovy b/src/test/groovy/graphql/execution/instrumentation/TestingExecuteObjectInstrumentationContext.groovy new file mode 100644 index 0000000000..a5b6cfd783 --- /dev/null +++ b/src/test/groovy/graphql/execution/instrumentation/TestingExecuteObjectInstrumentationContext.groovy @@ -0,0 +1,9 @@ +package graphql.execution.instrumentation + +class TestingExecuteObjectInstrumentationContext extends TestingInstrumentContext> implements ExecuteObjectInstrumentationContext { + + TestingExecuteObjectInstrumentationContext(Object op, Object executionList, Object throwableList, Boolean useOnDispatch) { + super(op, executionList, throwableList, useOnDispatch) + } +} + From 2ec535dbbd5babb3e7a63ecab5272f908ff91910 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 11 Sep 2023 17:46:46 +1000 Subject: [PATCH 002/393] A POC to show how ExecutionResult wrapping could be avoided -fix for newEC in subscriptions --- .../java/graphql/execution/SubscriptionExecutionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index 87f5d88ab8..ca0c7f98ae 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -136,7 +136,7 @@ private CompletableFuture executeSubscriptionEvent(ExecutionCon FieldValueInfo fieldValueInfo = completeField(newExecutionContext, newParameters, fetchedValue); CompletableFuture overallResult = fieldValueInfo .getFieldValueFuture() - .thenApply(val -> new ExecutionResultImpl(val, executionContext.getErrors())) + .thenApply(val -> new ExecutionResultImpl(val, newExecutionContext.getErrors())) .thenApply(executionResult -> wrapWithRootFieldName(newParameters, executionResult)); // dispatch instrumentation so they can know about each subscription event From e7a5582d33a3b12d6bf55121a87a3574abf8df1a Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 11 Sep 2023 18:33:18 +1000 Subject: [PATCH 003/393] A POC to show how ExecutionResult wrapping could be avoided - fixed more instrumentation callback lists --- .../instrumentation/LegacyTestingInstrumentation.groovy | 5 +++++ .../preparsed/PreparsedDocumentProviderTest.groovy | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy index e8a9478cb6..640aa32c56 100644 --- a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy @@ -61,6 +61,11 @@ class LegacyTestingInstrumentation implements Instrumentation { return new TestingExecutionStrategyInstrumentationContext("execution-strategy", executionList, throwableList, useOnDispatch) } + @Override + ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + return new TestingExecuteObjectInstrumentationContext("execute-object", executionList, throwableList, useOnDispatch) + } + @Override InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage diff --git a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy index 104d00ea28..330ce02dbc 100644 --- a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy +++ b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy @@ -39,7 +39,7 @@ class PreparsedDocumentProviderTest extends Specification { "end:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", @@ -48,7 +48,7 @@ class PreparsedDocumentProviderTest extends Specification { "end:complete-id", "end:field-id", - "end:execution-strategy", + "end:execute-object", "end:complete-hero", "end:field-hero", @@ -80,7 +80,7 @@ class PreparsedDocumentProviderTest extends Specification { "end:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", @@ -89,7 +89,7 @@ class PreparsedDocumentProviderTest extends Specification { "end:complete-id", "end:field-id", - "end:execution-strategy", + "end:execute-object", "end:complete-hero", "end:field-hero", From 8ec6cea344b166d8918879dd984f59bbe8ca8558 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 11 Sep 2023 19:13:36 +1000 Subject: [PATCH 004/393] A POC to show how ExecutionResult wrapping could be avoided - fixed more instrumentation callback lists and tests --- .../FieldLevelTrackingApproach.java | 61 ++++++++++--------- .../AllNullTestingInstrumentation.groovy | 7 +++ .../InstrumentationTest.groovy | 8 +-- 3 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 732582efb5..c3774acf9e 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -122,14 +122,8 @@ public InstrumentationState createState() { ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { CallStack callStack = (CallStack) rawState; - ResultPath path = parameters.getExecutionStrategyParameters().getPath(); - int parentLevel = path.getLevel(); - int curLevel = parentLevel + 1; - int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); - synchronized (callStack) { - callStack.increaseExpectedFetchCount(curLevel, fieldCount); - callStack.increaseHappenedStrategyCalls(curLevel); - } + int curLevel = getCurrentLevel(parameters); + increaseCallCounts(callStack, curLevel, parameters); return new ExecutionStrategyInstrumentationContext() { @Override @@ -144,13 +138,7 @@ public void onCompleted(ExecutionResult result, Throwable t) { @Override public void onFieldValuesInfo(List fieldValueInfoList) { - boolean dispatchNeeded; - synchronized (callStack) { - dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); - } - if (dispatchNeeded) { - dispatch(); - } + onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); } @Override @@ -164,14 +152,8 @@ public void onFieldValuesException() { ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { CallStack callStack = (CallStack) rawState; - ResultPath path = parameters.getExecutionStrategyParameters().getPath(); - int parentLevel = path.getLevel(); - int curLevel = parentLevel + 1; - int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); - synchronized (callStack) { - callStack.increaseExpectedFetchCount(curLevel, fieldCount); - callStack.increaseHappenedStrategyCalls(curLevel); - } + int curLevel = getCurrentLevel(parameters); + increaseCallCounts(callStack, curLevel, parameters); return new ExecuteObjectInstrumentationContext() { @@ -185,13 +167,7 @@ public void onCompleted(Map result, Throwable t) { @Override public void onFieldValuesInfo(List fieldValueInfoList) { - boolean dispatchNeeded; - synchronized (callStack) { - dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); - } - if (dispatchNeeded) { - dispatch(); - } + onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); } @Override @@ -203,6 +179,31 @@ public void onFieldValuesException() { }; } + private int getCurrentLevel(InstrumentationExecutionStrategyParameters parameters) { + ResultPath path = parameters.getExecutionStrategyParameters().getPath(); + return path.getLevel() + 1; + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private static void increaseCallCounts(CallStack callStack, int curLevel, InstrumentationExecutionStrategyParameters parameters) { + int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); + synchronized (callStack) { + callStack.increaseExpectedFetchCount(curLevel, fieldCount); + callStack.increaseHappenedStrategyCalls(curLevel); + } + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private void onFieldValuesInfoDispatchIfNeeded(CallStack callStack, List fieldValueInfoList, int curLevel) { + boolean dispatchNeeded; + synchronized (callStack) { + dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); + } + if (dispatchNeeded) { + dispatch(); + } + } + // // thread safety : called with synchronised(callStack) // diff --git a/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy index 87ef7800f0..c430c10851 100644 --- a/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy @@ -60,6 +60,13 @@ class AllNullTestingInstrumentation implements Instrumentation { return null } + @Override + ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + assert state == instrumentationState + executionList << "start:execute-object" + return null + } + @Override InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { assert state == instrumentationState diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index a6c7968cf7..9da4e4990c 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -58,7 +58,7 @@ class InstrumentationTest extends Specification { "onDispatched:fetch-hero", "end:fetch-hero", "start:complete-hero", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", "onDispatched:fetch-id", @@ -68,8 +68,8 @@ class InstrumentationTest extends Specification { "end:complete-id", "onDispatched:field-id", "end:field-id", - "onDispatched:execution-strategy", - "end:execution-strategy", + "onDispatched:execute-object", + "end:execute-object", "onDispatched:complete-hero", "end:complete-hero", "onDispatched:field-hero", @@ -394,7 +394,7 @@ class InstrumentationTest extends Specification { "start:field-human", "start:fetch-human", "start:complete-human", - "start:execution-strategy", + "start:execute-object", "start:field-id", "start:fetch-id", "start:complete-id", From b16deb407ecea2e02703cf56b6a720704ed46fc4 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 12 Sep 2023 09:40:32 +1000 Subject: [PATCH 005/393] A POC to show how ExecutionResult wrapping could be avoided - javadoc fix plus test fix up - still failing but one --- src/main/java/graphql/execution/ExecutionStrategy.java | 2 +- .../graphql/execution/ExecutionStrategyErrorsTest.groovy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 2bf82ee7ce..7aac904342 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -191,7 +191,7 @@ public static String mkNameForPath(List currentField) { * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * - * @return a promise to an {@link Map} + * @return a promise to a map of object field values * * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy index b2ffa9e3cf..79a6ae965a 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy @@ -70,11 +70,11 @@ class ExecutionStrategyErrorsTest extends Specification { Instrumentation instrumentation = new SimplePerformantInstrumentation() { @Override - InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { if (parameters.field.name == "diceyListCallAbort") { throw new AbortExecutionException("No lists for you") } - return super.beginFieldListComplete(parameters, state) + return super.beginFieldListCompletion(parameters, state) } } def graphQL = GraphQL.newGraphQL(schema).instrumentation(instrumentation).build() From bab21cdf5107e89eb3f314b7413d32ef1e9d3d9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 16:51:59 +0000 Subject: [PATCH 006/393] Bump actions/setup-node from 3 to 4 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/invoke_test_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index f30e105394..6d267c4f5a 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: '14' - run: npm install --prefix .github/workflows From 5e0842d5a3feb3488b417107440ec8eee8263b21 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 24 Oct 2023 08:29:24 +1100 Subject: [PATCH 007/393] Add GitHub action to manage stale PRs and issues --- .github/workflows/stale-pr-issue.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/stale-pr-issue.yml diff --git a/.github/workflows/stale-pr-issue.yml b/.github/workflows/stale-pr-issue.yml new file mode 100644 index 0000000000..af1359b548 --- /dev/null +++ b/.github/workflows/stale-pr-issue.yml @@ -0,0 +1,44 @@ +# Mark inactive issues and PRs as stale +# GitHub action based on https://github.com/actions/stale + +name: 'Close stale issues and PRs' +on: + schedule: + # Execute every day + - cron: '0 0 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + close-pending: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + # GLOBAL ------------------------------------------------------------ + # Exempt any PRs or issues already added to a milestone + exempt-all-milestones: true + # Days until issues or pull requests are labelled as stale + days-before-stale: 60 + + # ISSUES ------------------------------------------------------------ + # Issues will be closed after 90 days of inactive (60 to mark as stale + 30 to close) + days-before-issue-close: 30 + stale-issue-message: > + Hello, this issue has been inactive for 60 days, so we're marking it as stale. + If you would like to continue this discussion, please comment within the next 30 days or we'll close the issue. + close-issue-message: > + Hello, as this issue has been inactive for 90 days, we're closing the issue. + If you would like to resume the discussion, please create a new issue. + + # PULL REQUESTS ----------------------------------------------------- + # PRs will be closed after 90 days of inactive (60 to mark as stale + 30 to close) + days-before-pr-close: 30 + stale-pr-message: > + Hello, this pull request has been inactive for 60 days, so we're marking it as stale. + If you would like to continue working on this pull request, please make an update within the next 30 days, or we'll close the pull request. + close-pr-message: > + Hello, as this pull request has been inactive for 90 days, we're closing this pull request. + We always welcome contributions, and if you would like to continue, please open a new pull request. \ No newline at end of file From 64931e7c65b0a862ea76ec0db7b1784db3052888 Mon Sep 17 00:00:00 2001 From: Salvo Miosi Date: Tue, 24 Oct 2023 16:40:28 +0200 Subject: [PATCH 008/393] Add 'compute' family of methods to GraphQLContext --- src/main/java/graphql/GraphQLContext.java | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/main/java/graphql/GraphQLContext.java b/src/main/java/graphql/GraphQLContext.java index 081c17725f..8b913919d3 100644 --- a/src/main/java/graphql/GraphQLContext.java +++ b/src/main/java/graphql/GraphQLContext.java @@ -5,7 +5,9 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Stream; import static graphql.Assert.assertNotNull; @@ -171,6 +173,52 @@ public GraphQLContext putAll(Consumer contextBuilderCons return putAll(builder); } + /** + * Attempts to compute a mapping for the specified key and its + * current mapped value (or null if there is no current mapping). + * + * @param key key with which the specified value is to be associated + * @param remappingFunction the function to compute a value + * + * @return the new value associated with the specified key, or null if none + * @param for two + */ + public T compute(Object key, BiFunction remappingFunction) { + assertNotNull(remappingFunction); + return (T) map.compute(assertNotNull(key), (k, v) -> remappingFunction.apply(k, (T) v)); + } + + /** + * If the specified key is not already associated with a value (or is mapped to null), + * attempts to compute its value using the given mapping function and enters it into this map unless null. + * + * @param key key with which the specified value is to be associated + * @param mappingFunction the function to compute a value + * + * @return the current (existing or computed) value associated with the specified key, or null if the computed value is null + * @param for two + */ + + public T computeIfAbsent(Object key, Function mappingFunction) { + return (T) map.computeIfAbsent(assertNotNull(key), assertNotNull(mappingFunction)); + } + + /** + * If the value for the specified key is present and non-null, + * attempts to compute a new mapping given the key and its current mapped value. + * + * @param key key with which the specified value is to be associated + * @param remappingFunction the function to compute a value + * + * @return the new value associated with the specified key, or null if none + * @param for two + */ + + public T computeIfPresent(Object key, BiFunction remappingFunction) { + assertNotNull(remappingFunction); + return (T) map.computeIfPresent(assertNotNull(key), (k, v) -> remappingFunction.apply(k, (T) v)); + } + /** * @return a stream of entries in the context */ From 72621eca9a7ab862075a2b6300b1a2a68f856b40 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 25 Oct 2023 13:26:22 +1100 Subject: [PATCH 009/393] Add test that reproduces the bug --- .../execution/ValuesResolverTest.groovy | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 0e53791a3b..58832deedb 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -427,6 +427,48 @@ class ValuesResolverTest extends Specification { | CoercedVariables.of(["var": [:]]) } + def "getArgumentValues: invalid oneOf input validation applied with nested input types"() { + given: "schema defining input object" + def oneOfObjectType = newInputObject() + .name("OneOfInputObject") + .withAppliedDirective(Directives.OneOfDirective.toAppliedDirective()) + .field(newInputObjectField() + .name("a") + .type(GraphQLString) + .build()) + .field(newInputObjectField() + .name("b") + .type(GraphQLInt) + .build()) + .build() + + def parentObjectType = newInputObject() + .name("ParentInputObject") + .field(newInputObjectField() + .name("oneOfField") + .type(oneOfObjectType) + .build()) + .build() + + def inputValue = buildObjectLiteral([ + oneOfField: [ + a: StringValue.of("abc"), + b: IntValue.of(123) + ] + ]) + + def argument = new Argument("arg", inputValue) + + when: + def fieldArgument = newArgument().name("arg").type(parentObjectType).build() + ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) + + then: + def e = thrown(OneOfTooManyKeysException) + e.message == "Exactly one key must be specified for OneOf type 'oneOfInputObject'." + + } + def "getArgumentValues: invalid oneOf input because of null value - #testCase"() { given: "schema defining input object" def inputObjectType = newInputObject() From f1dd89e508c389fd82c435515491edb3cc0ba052 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 26 Oct 2023 15:43:50 +1100 Subject: [PATCH 010/393] Apply oneOf validation to nested input fields --- .../graphql/execution/ValuesResolver.java | 37 +----- .../execution/ValuesResolverConversion.java | 2 - .../ValuesResolverOneOfValidation.java | 79 ++++++++++++ .../execution/ValuesResolverTest.groovy | 112 ++++++++++++++++-- 4 files changed, 188 insertions(+), 42 deletions(-) create mode 100644 src/main/java/graphql/execution/ValuesResolverOneOfValidation.java diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index 27487ca556..e269a63b4c 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -376,41 +376,14 @@ private static Map getArgumentValuesImpl( locale); coercedValues.put(argumentName, value); } - // @oneOf input must be checked now that all variables and literals have been converted - GraphQLType unwrappedType = GraphQLTypeUtil.unwrapNonNull(argumentType); - if (unwrappedType instanceof GraphQLInputObjectType) { - GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedType; - if (inputObjectType.isOneOf() && ! ValuesResolverConversion.isNullValue(value)) { - validateOneOfInputTypes(inputObjectType, argumentValue, argumentName, value, locale); - } - } - } - } - return coercedValues; - } + ValuesResolverOneOfValidation.validateOneOfInputTypes(argumentType, value, argumentValue, argumentName, locale); - @SuppressWarnings("unchecked") - private static void validateOneOfInputTypes(GraphQLInputObjectType oneOfInputType, Value argumentValue, String argumentName, Object inputValue, Locale locale) { - Assert.assertTrue(inputValue instanceof Map, () -> String.format("The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName)); - Map objectMap = (Map) inputValue; - int mapSize; - if (argumentValue instanceof ObjectValue) { - mapSize = ((ObjectValue) argumentValue).getObjectFields().size(); - } else { - mapSize = objectMap.size(); - } - if (mapSize != 1) { - String msg = I18n.i18n(I18n.BundleType.Execution, locale) - .msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName()); - throw new OneOfTooManyKeysException(msg); - } - String fieldName = objectMap.keySet().iterator().next(); - if (objectMap.get(fieldName) == null) { - String msg = I18n.i18n(I18n.BundleType.Execution, locale) - .msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName); - throw new OneOfNullValueException(msg); + } } + + + return coercedValues; } private static Map argumentMap(List arguments) { diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index eff4d1cef1..29c3edaf2d 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -104,7 +104,6 @@ static Object valueToLiteralImpl(GraphqlFieldVisibility fieldVisibility, * @param type the type of input value * @param graphqlContext the GraphqlContext to use * @param locale the Locale to use - * * @return a value converted to an internal value */ static Object externalValueToInternalValue(GraphqlFieldVisibility fieldVisibility, @@ -596,7 +595,6 @@ private static List externalValueToInternalValueForList( * @param coercedVariables the coerced variable values * @param graphqlContext the GraphqlContext to use * @param locale the Locale to use - * * @return literal converted to an internal value */ static Object literalToInternalValue( diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java new file mode 100644 index 0000000000..3f9813d1d8 --- /dev/null +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -0,0 +1,79 @@ +package graphql.execution; + +import graphql.Assert; +import graphql.i18n.I18n; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.Value; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +final class ValuesResolverOneOfValidation { + + @SuppressWarnings("unchecked") + static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value argumentValue, String argumentName, Locale locale) { + // @oneOf input must be checked now that all variables and literals have been converted + GraphQLType unwrappedType = GraphQLTypeUtil.unwrapNonNull(type); + + if (unwrappedType instanceof GraphQLInputObjectType && !ValuesResolverConversion.isNullValue(inputValue)) { + Assert.assertTrue(inputValue instanceof Map, () -> String.format("The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName)); + Map objectMap = (Map) inputValue; + + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedType; + + if (inputObjectType.isOneOf()) { + validateOneOfInputTypesInternal(inputObjectType, argumentValue, objectMap, locale); + } + + for (GraphQLInputObjectField fieldDefinition : inputObjectType.getFields()) { + GraphQLInputType childFieldType = fieldDefinition.getType(); + String childFieldName = fieldDefinition.getName(); + Object childFieldInputValue = objectMap.get(childFieldName); + + if (argumentValue instanceof ObjectValue) { + List values = ((ObjectValue) argumentValue).getObjectFields().stream() + .filter(of -> of.getName().equals(childFieldName)) + .map(ObjectField::getValue) + .collect(Collectors.toList()); + + if (values.size() > 1) { + Assert.assertShouldNeverHappen(String.format("argument %s has %s object fields with the same name: '%s'. A maximum of 1 is expected", argumentName, values.size(), childFieldName)); + } else if (!values.isEmpty()) { + validateOneOfInputTypes(childFieldType, childFieldInputValue, values.get(0), argumentName, locale); + } + } else { + validateOneOfInputTypes(childFieldType, childFieldInputValue, argumentValue, argumentName, locale); + } + } + } + } + + private static void validateOneOfInputTypesInternal(GraphQLInputObjectType oneOfInputType, Value argumentValue, Map objectMap, Locale locale) { + int mapSize; + + if (argumentValue instanceof ObjectValue) { + mapSize = ((ObjectValue) argumentValue).getObjectFields().size(); + } else { + mapSize = objectMap.size(); + } + if (mapSize != 1) { + String msg = I18n.i18n(I18n.BundleType.Execution, locale) + .msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName()); + throw new OneOfTooManyKeysException(msg); + } + String fieldName = objectMap.keySet().iterator().next(); + if (objectMap.get(fieldName) == null) { + String msg = I18n.i18n(I18n.BundleType.Execution, locale) + .msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName); + throw new OneOfNullValueException(msg); + } + } +} diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 58832deedb..92cd2b1ce7 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -427,7 +427,7 @@ class ValuesResolverTest extends Specification { | CoercedVariables.of(["var": [:]]) } - def "getArgumentValues: invalid oneOf input validation applied with nested input types"() { + def "getArgumentValues: invalid oneOf nested input because of duplicate keys - #testCase"() { given: "schema defining input object" def oneOfObjectType = newInputObject() .name("OneOfInputObject") @@ -450,22 +450,118 @@ class ValuesResolverTest extends Specification { .build()) .build() - def inputValue = buildObjectLiteral([ + def argument = new Argument("arg", inputValue) + + when: + def fieldArgument = newArgument().name("arg").type(parentObjectType).build() + ValuesResolver.getArgumentValues([fieldArgument], [argument], variables, graphQLContext, locale) + + then: + def e = thrown(OneOfTooManyKeysException) + e.message == "Exactly one key must be specified for OneOf type 'OneOfInputObject'." + + where: + testCase | inputValue | variables + '{oneOfField: {a: "abc", b: 123} } {}' | buildObjectLiteral([ oneOfField: [ a: StringValue.of("abc"), b: IntValue.of(123) ] - ]) + ]) | CoercedVariables.emptyVariables() + '{oneOfField: {a: null, b: 123 }} {}' | buildObjectLiteral([ + oneOfField: [ + a: NullValue.of(), + b: IntValue.of(123) + ] + ]) | CoercedVariables.emptyVariables() + + '{oneOfField: {a: $var, b: 123 }} { var: null }' | buildObjectLiteral([ + oneOfField: [ + a: VariableReference.of("var"), + b: IntValue.of(123) + ] + ]) | CoercedVariables.of(["var": null]) + + '{oneOfField: {a: $var, b: 123 }} {}' | buildObjectLiteral([ + oneOfField: [ + a: VariableReference.of("var"), + b: IntValue.of(123) + ] + ]) | CoercedVariables.emptyVariables() + + '{oneOfField: {a : "abc", b : null}} {}' | buildObjectLiteral([ + oneOfField: [ + a: StringValue.of("abc"), + b: NullValue.of() + ] + ]) | CoercedVariables.emptyVariables() + + '{oneOfField: {a : null, b : null}} {}' | buildObjectLiteral([ + oneOfField: [ + a: NullValue.of(), + b: NullValue.of() + ] + ]) | CoercedVariables.emptyVariables() + + '{oneOfField: {a : $a, b : $b}} {a : "abc"}' | buildObjectLiteral([ + oneOfField: [ + a: VariableReference.of("a"), + b: VariableReference.of("v") + ] + ]) | CoercedVariables.of(["a": "abc"]) + '$var {var : {oneOfField: { a : "abc", b : 123}}}' | VariableReference.of("var") + | CoercedVariables.of(["var": ["oneOfField": ["a": "abc", "b": 123]]]) + + '$var {var : {oneOfField: {} }}' | VariableReference.of("var") + | CoercedVariables.of(["var": ["oneOfField": [:]]]) + + } + + def "getArgumentValues: invalid oneOf nested input because of null value - #testCase"() { + given: "schema defining input object" + def oneOfObjectType = newInputObject() + .name("OneOfInputObject") + .withAppliedDirective(Directives.OneOfDirective.toAppliedDirective()) + .field(newInputObjectField() + .name("a") + .type(GraphQLString) + .build()) + .field(newInputObjectField() + .name("b") + .type(GraphQLInt) + .build()) + .build() + + def parentObjectType = newInputObject() + .name("ParentInputObject") + .field(newInputObjectField() + .name("oneOfField") + .type(oneOfObjectType) + .build()) + .build() - def argument = new Argument("arg", inputValue) - when: def fieldArgument = newArgument().name("arg").type(parentObjectType).build() - ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) + + when: + def argument = new Argument("arg", inputValue) + ValuesResolver.getArgumentValues([fieldArgument], [argument], variables, graphQLContext, locale) then: - def e = thrown(OneOfTooManyKeysException) - e.message == "Exactly one key must be specified for OneOf type 'oneOfInputObject'." + def e = thrown(OneOfNullValueException) + e.message == "OneOf type field 'OneOfInputObject.a' must be non-null." + + where: + // from https://github.com/graphql/graphql-spec/pull/825/files#diff-30a69c5a5eded8e1aea52e53dad1181e6ec8f549ca2c50570b035153e2de1c43R1692 + testCase | inputValue | variables + + '`{ oneOfField: { a: null }}` {}' | buildObjectLiteral([ + oneOfField: [a: NullValue.of()] + ]) | CoercedVariables.emptyVariables() + + '`{ oneOfField: { a: $var }}` { var : null}' | buildObjectLiteral([ + oneOfField: [a: VariableReference.of("var")] + ]) | CoercedVariables.of(["var": null]) } From 18854379e0b11386bb7d876ec89d0803b1182103 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 26 Oct 2023 15:48:48 +1100 Subject: [PATCH 011/393] Remove comment --- .../java/graphql/execution/ValuesResolverOneOfValidation.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index 3f9813d1d8..a024e44307 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -20,7 +20,6 @@ final class ValuesResolverOneOfValidation { @SuppressWarnings("unchecked") static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value argumentValue, String argumentName, Locale locale) { - // @oneOf input must be checked now that all variables and literals have been converted GraphQLType unwrappedType = GraphQLTypeUtil.unwrapNonNull(type); if (unwrappedType instanceof GraphQLInputObjectType && !ValuesResolverConversion.isNullValue(inputValue)) { From 60d7da6814aca3d8bcd4147713be5476fe26426e Mon Sep 17 00:00:00 2001 From: Jordie Biemold Date: Fri, 27 Oct 2023 19:51:35 +0200 Subject: [PATCH 012/393] Update German translations --- src/main/resources/i18n/Execution_de.properties | 8 ++++++++ src/main/resources/i18n/Parsing_de.properties | 4 +++- src/main/resources/i18n/Validation_de.properties | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/i18n/Execution_de.properties diff --git a/src/main/resources/i18n/Execution_de.properties b/src/main/resources/i18n/Execution_de.properties new file mode 100644 index 0000000000..aff0b11d00 --- /dev/null +++ b/src/main/resources/i18n/Execution_de.properties @@ -0,0 +1,8 @@ +# +# This resource bundle is used for the query execution code to produce i18n messages +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +Execution.handleOneOfNotOneFieldError=Es muss genau ein Schlüssel angegeben werden für OneOf Typ ''{0}''. +Execution.handleOneOfValueIsNullError=OneOf type field ''{0}'' darf nicht null sein. diff --git a/src/main/resources/i18n/Parsing_de.properties b/src/main/resources/i18n/Parsing_de.properties index 919e5fee9c..9877badacf 100644 --- a/src/main/resources/i18n/Parsing_de.properties +++ b/src/main/resources/i18n/Parsing_de.properties @@ -18,7 +18,9 @@ InvalidSyntaxBail.full=Ung # InvalidSyntaxMoreTokens.full=Es wurde eine ungültige Syntax festgestellt. Es gibt zusätzliche Token im Text, die nicht konsumiert wurden. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} # -ParseCancelled.full=Es wurden mehr als {0} ''{1}'' Token präsentiert. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen +ParseCancelled.full=Es wurden mehr als {0} ''{1}'' Token präsentiert. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. +ParseCancelled.tooDeep=Es wurden mehr als {0} tief ''{1}'' Regeln ausgeführt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. +ParseCancelled.tooManyChars=Es wurden mehr als {0} Zeichen vorgelegt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. # InvalidUnicode.trailingLeadingSurrogate=Ungültiger Unicode gefunden. Trailing surrogate muss ein leading surrogate vorangestellt werden. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} InvalidUnicode.leadingTrailingSurrogate=Ungültiger Unicode gefunden. Auf ein leading surrogate muss ein trailing surrogate folgen. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} diff --git a/src/main/resources/i18n/Validation_de.properties b/src/main/resources/i18n/Validation_de.properties index c15e3bb550..41ddd049a5 100644 --- a/src/main/resources/i18n/Validation_de.properties +++ b/src/main/resources/i18n/Validation_de.properties @@ -77,6 +77,7 @@ VariableDefaultValuesOfCorrectType.badDefault=Validierungsfehler ({0}) : Ung VariablesAreInputTypes.wrongType=Validierungsfehler ({0}) : Eingabevariable ''{1}'' Typ ''{2}'' ist kein Eingabetyp # VariableTypesMatchRule.unexpectedType=Validierungsfehler ({0}) : Variable ''{1}'' vom Typ ''{2}'' verwendet in Position, die Typ ''{3}'' erwartet +UniqueObjectFieldName.duplicateFieldName=Validierungsfehler ({0}) : Es kann nur ein Feld mit Name ''{1}'' geben # # These are used but IDEA cant find them easily as being called # From e1049e23fb183d45a6172951ec6a9b9425ed1e58 Mon Sep 17 00:00:00 2001 From: Jordie Biemold Date: Sun, 27 Nov 2022 15:56:28 +0100 Subject: [PATCH 013/393] Initial Dutch translations --- src/main/resources/i18n/Parsing_nl.properties | 26 +++++ src/main/resources/i18n/Scalars_nl.properties | 29 ++++++ .../resources/i18n/Validation_nl.properties | 99 +++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 src/main/resources/i18n/Parsing_nl.properties create mode 100644 src/main/resources/i18n/Scalars_nl.properties create mode 100644 src/main/resources/i18n/Validation_nl.properties diff --git a/src/main/resources/i18n/Parsing_nl.properties b/src/main/resources/i18n/Parsing_nl.properties new file mode 100644 index 0000000000..8761cef50c --- /dev/null +++ b/src/main/resources/i18n/Parsing_nl.properties @@ -0,0 +1,26 @@ +# +# This resource bundle is used for the query parsing code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +InvalidSyntax.noMessage=Ongeldige syntax op lijn {0} kolom {1} +InvalidSyntax.full=Ongeldige syntax, ANTLR foutmelding ''{0}'' op lijn {1} kolom {2} + +InvalidSyntaxBail.noToken=Ongeldige syntax op lijn {0} kolom {1} +InvalidSyntaxBail.full=Ongeldige syntax wegens ongeldige token ''{0}'' op lijn {1} kolom {2} +# +InvalidSyntaxMoreTokens.full=Ongeldige syntax tegengekomen. Er zijn tokens in de tekst die niet zijn verwerkt. Ongeldige token ''{0}'' op lijn {1} kolom {2} +# +ParseCancelled.full=Meer dan {0} ''{1}'' tokens zijn gepresenteerd. Om een DDoS-aanval te voorkomen, is het parsen gestopt. +# +InvalidUnicode.trailingLeadingSurrogate=Ongeldige unicode tegengekomen. Trailing surrogate moet vooropgaan aan een leading surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidUnicode.leadingTrailingSurrogate=Ongeldige unicode tegengekomen. Leading surrogate moet voorafgaan aan een trailing surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidUnicode.invalidCodePoint=Ongeldige unicode tegengekomen. Ongeldig code point. Ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidUnicode.incorrectEscape=Ongeldige unicode tegengekomen. Ongeldige geformatteerde escape-sequentie. Ongeldige token ''{0}'' op lijn {1} kolom {2}} diff --git a/src/main/resources/i18n/Scalars_nl.properties b/src/main/resources/i18n/Scalars_nl.properties new file mode 100644 index 0000000000..047bb692b6 --- /dev/null +++ b/src/main/resources/i18n/Scalars_nl.properties @@ -0,0 +1,29 @@ +# +# This resource bundle is used for the scalar code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +Scalar.unexpectedAstType=Verwacht werd een AST type ''{0}'', maar het was een ''{1}'' +# +Enum.badInput=Ongeldige invoer voor enum ''{0}''. Onbekende waarde ''{1}'' +Enum.badName=Ongeldige invoer voor enum ''{0}''. Geen waarde gevonden voor naam ''{1}'' +Enum.unallowableValue=Literal is niet een toegestaande waarde voor enum ''{0}'' - ''{1}'' +# +Int.notInt=Verwacht werd een waarde die in ''Int'' veranderd kon worden, maar het was een ''{0}'' +Int.outsideRange=Verwacht werd een waarde die binnen het integerbereik valt, maar het was een ''{0}'' +# +ID.notId=Verwacht werd een waarde die in ''ID'' veranderd kon worden, maar het was een ''{0}'' +ID.unexpectedAstType=Verwacht werd een AST type ''IntValue'' of ''StringValue'', maar het was een ''{0}'' +# +Float.notFloat=Verwacht werd een waarde die in ''Float'' veranderd kon worden, maar het was een ''{0}'' +Float.unexpectedAstType=Verwacht werd een AST type ''IntValue'' of ''FloatValue'', maar het was een ''{0}'' +# +Boolean.notBoolean=Verwacht werd een waarde die in ''Boolean'' veranderd kon worden, maar het was een ''{0}'' +Boolean.unexpectedAstType=Verwacht werd een AST type ''BooleanValue'', maar het was een ''{0}'' diff --git a/src/main/resources/i18n/Validation_nl.properties b/src/main/resources/i18n/Validation_nl.properties new file mode 100644 index 0000000000..5e7c4afb56 --- /dev/null +++ b/src/main/resources/i18n/Validation_nl.properties @@ -0,0 +1,99 @@ +# +# This resource bundle is used for the query validation code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +ExecutableDefinitions.notExecutableType=Validatiefout ({0}) : Type definitie ''{1}'' is niet uitvoerbaar +ExecutableDefinitions.notExecutableSchema=Validatiefout ({0}) : Schema definitie is niet uitvoerbaar +ExecutableDefinitions.notExecutableDirective=Validatiefout ({0}) : Directive defnitie ''{1}'' is niet uitvoerbaar +ExecutableDefinitions.notExecutableDefinition=Validatiefout ({0}) : Aangeleverde definition is niet uitvoerbaar +# +FieldsOnCorrectType.unknownField=Validatiefout ({0}) : Veld ''{1}'' in type ''{2}'' is ongedefinieerd +# +FragmentsOnCompositeType.invalidInlineTypeCondition=Validatiefout ({0}) : Inline fragment type condition is ongeldig, moet op een Object/Interface/Union zitten +FragmentsOnCompositeType.invalidFragmentTypeCondition=Validatiefout ({0}) : Fragment type condition is ongeldig, moet op een Object/Interface/Union zitten +# +KnownArgumentNames.unknownDirectiveArg=Validatiefout ({0}) : Onbekende directive argument ''{1}'' +KnownArgumentNames.unknownFieldArg=Validatiefout ({0}) : Onbekende field argument ''{1}'' +# +KnownDirectives.unknownDirective=Validatiefout ({0}) : Onbekende directive ''{1}'' +KnownDirectives.directiveNotAllowed=Validatiefout ({0}) : Directive ''{1}'' is hier niet toegestaan +# +KnownFragmentNames.undefinedFragment=Validatiefout ({0}) : Ongedefinieerd fragment ''{1}'' +# +KnownTypeNames.unknownType=Validatiefout ({0}) : Ongeldig type ''{1}'' +# +LoneAnonymousOperation.withOthers=Validatiefout ({0}) : Anonieme operation met andere operations +LoneAnonymousOperation.namedOperation=Validatiefout ({0}) : Operation ''{1}'' volgt een anonieme operatie +# +NoFragmentCycles.cyclesNotAllowed=Validatiefout ({0}) : Fragment cycles niet toegestaan +# +NoUndefinedVariables.undefinedVariable=Validatiefout ({0}) : Ongedefinieerde variable ''{1}'' +# +NoUnusedFragments.unusedFragments=Validatiefout ({0}) : Ongebruikt fragment ''{1}'' +# +NoUnusedVariables.unusedVariable=Validatiefout ({0}) : Ongebruikte variabele ''{1}'' +# +OverlappingFieldsCanBeMerged.differentFields=Validatiefout ({0}) : ''{1}'' : ''{2}'' en ''{3}'' zijn verschillende velden +OverlappingFieldsCanBeMerged.differentArgs=Validatiefout ({0}) : ''{1}'' : velden hebben verschillende argumenten +OverlappingFieldsCanBeMerged.differentNullability=Validatiefout ({0}) : ''{1}'' : velden hebben verschillende nullability shapes +OverlappingFieldsCanBeMerged.differentLists=Validatiefout ({0}) : ''{1}'' : velden hebben verschillende vormen +OverlappingFieldsCanBeMerged.differentReturnTypes=Validatiefout ({0}) : ''{1}'' : retourneert verschillende types ''{2}'' en ''{3}'' +# +PossibleFragmentSpreads.inlineIncompatibleTypes=Validatiefout ({0}) : Fragment kan hier niet uitgespreid worden omdat een object van type ''{1}'' nooit van het type ''{2}'' kan zijn +PossibleFragmentSpreads.fragmentIncompatibleTypes=Validatiefout ({0}) : Fragment ''{1}'' kan hier niet uitgespreid worden omdat een object van type ''{2}'' nooit van het type ''{3}'' kan zijn +# +ProvidedNonNullArguments.missingFieldArg=Validatiefout ({0}) : Missend field argument ''{1}'' +ProvidedNonNullArguments.missingDirectiveArg=Validatiefout ({0}) : Missend directive argument ''{1}'' +ProvidedNonNullArguments.nullValue=Validatiefout ({0}) : Null waarde voor non-null field argument ''{1}'' +# +ScalarLeaves.subselectionOnLeaf=Validatiefout ({0}) : Selectie niet toegestaan op leaf type ''{1}'' van veld ''{2}'' +ScalarLeaves.subselectionRequired=Validatiefout ({0}) : Selectie verplicht voor type ''{1}'' van veld ''{2}'' +# +SubscriptionUniqueRootField.multipleRootFields=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field hebben +SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field met fragmenten hebben +SubscriptionIntrospectionRootField.introspectionRootField=Validatiefout ({0}) : Subscription operation ''{1}'' root field ''{2}'' kan geen introspectieveld zijn +SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' kan geen introspectieveld zijn +# +UniqueArgumentNames.uniqueArgument=Validatiefout ({0}) : Er mag maar één argument met naam ''{1}'' bestaan +# +UniqueDirectiveNamesPerLocation.uniqueDirectives=Validatiefout ({0}) : Onherhaalbare directives moeten een unieke naam hebben binnen een locatie. De directive ''{1}'' gebruikt op een ''{2}'' is niet uniek +# +UniqueFragmentNames.oneFragment=Validatiefout ({0}) : Er mag maar één fragment met naam ''{1}'' bestaan +# +UniqueOperationNames.oneOperation=Validatiefout ({0}) : Er mag maar één operatie met naam ''{1}'' bestaan +# +UniqueVariableNames.oneVariable=Validatiefout ({0}) : Er mag maar één variabele met naam ''{1}'' bestaan +# +VariableDefaultValuesOfCorrectType.badDefault=Validatiefout ({0}) : Ongeldige standaardwaarde ''{1}'' voor type ''{2}'' +# +VariablesAreInputTypes.wrongType=Validatiefout ({0}) : Invoervariabele ''{1}'' type ''{2}'' is geen invoertype +# +VariableTypesMatchRule.unexpectedType=Validatiefout ({0}) : Variable type ''{1}'' komt niet overeen met het verwachte type ''{2}'' +# +# These are used but IDEA cant find them easily as being called +# +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleNullError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' mag niet null zijn +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleScalarError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' is geen geldige ''{3}'' +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleScalarErrorCustomMessage=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' is geen geldige ''{3}'' - {4} +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleEnumError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' is geen geldige ''{3}'' +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleEnumErrorCustomMessage=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' is geen geldige ''{3}'' - {4} +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleNotObjectError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' moet een object type zijn +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleMissingFieldsError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' mist een verplicht veld ''{3}'' +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleExtraFieldError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' bevalt een veld niet in ''{3}'': ''{4}'' +# From 184c52adce443361ffc8303ec7bc4a5fa3b10618 Mon Sep 17 00:00:00 2001 From: Jordie Biemold Date: Fri, 27 Oct 2023 19:48:50 +0200 Subject: [PATCH 014/393] Update Dutch translations --- .../resources/i18n/Execution_nl.properties | 8 +++++++ src/main/resources/i18n/Parsing_nl.properties | 22 ++++++++++--------- src/main/resources/i18n/Scalars_nl.properties | 10 ++++----- .../resources/i18n/Validation_nl.properties | 7 +++--- 4 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/main/resources/i18n/Execution_nl.properties diff --git a/src/main/resources/i18n/Execution_nl.properties b/src/main/resources/i18n/Execution_nl.properties new file mode 100644 index 0000000000..f544873152 --- /dev/null +++ b/src/main/resources/i18n/Execution_nl.properties @@ -0,0 +1,8 @@ +# +# This resource bundle is used for the query execution code to produce i18n messages +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +Execution.handleOneOfNotOneFieldError=Er moet exact één sleutel aangegeven worden voor OneOf type ''{0}''. +Execution.handleOneOfValueIsNullError=OneOf type field ''{0}'' mag niet null zijn. diff --git a/src/main/resources/i18n/Parsing_nl.properties b/src/main/resources/i18n/Parsing_nl.properties index 8761cef50c..830bae5f6a 100644 --- a/src/main/resources/i18n/Parsing_nl.properties +++ b/src/main/resources/i18n/Parsing_nl.properties @@ -10,17 +10,19 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # -InvalidSyntax.noMessage=Ongeldige syntax op lijn {0} kolom {1} -InvalidSyntax.full=Ongeldige syntax, ANTLR foutmelding ''{0}'' op lijn {1} kolom {2} +InvalidSyntax.noMessage=Ongeldige syntaxis op lijn {0} kolom {1} +InvalidSyntax.full=Ongeldige syntaxis, ANTLR foutmelding ''{0}'' op lijn {1} kolom {2} -InvalidSyntaxBail.noToken=Ongeldige syntax op lijn {0} kolom {1} -InvalidSyntaxBail.full=Ongeldige syntax wegens ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidSyntaxBail.noToken=Ongeldige syntaxis op lijn {0} kolom {1} +InvalidSyntaxBail.full=Ongeldige syntaxis wegens ongeldige token ''{0}'' op lijn {1} kolom {2} # -InvalidSyntaxMoreTokens.full=Ongeldige syntax tegengekomen. Er zijn tokens in de tekst die niet zijn verwerkt. Ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidSyntaxMoreTokens.full=Ongeldige syntaxis tegengekomen. Er zijn tokens in de tekst die niet zijn verwerkt. Ongeldige token ''{0}'' op lijn {1} kolom {2} # -ParseCancelled.full=Meer dan {0} ''{1}'' tokens zijn gepresenteerd. Om een DDoS-aanval te voorkomen, is het parsen gestopt. +ParseCancelled.full=Meer dan {0} ''{1}'' tokens zijn gepresenteerd. Om een DDoS-aanval te voorkomen is het parsen gestopt. +ParseCancelled.tooDeep=Meer dan {0} diep, ''{1}'' regels zijn uitgevoerd. Om een DDoS-aanval te voorkomen is het parsen gestopt. +ParseCancelled.tooManyChars=Meer dan {0} tekens zijn voorgelegd. Om een DDoS-aanval te voorkomen is het parsen gestopt. # -InvalidUnicode.trailingLeadingSurrogate=Ongeldige unicode tegengekomen. Trailing surrogate moet vooropgaan aan een leading surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2} -InvalidUnicode.leadingTrailingSurrogate=Ongeldige unicode tegengekomen. Leading surrogate moet voorafgaan aan een trailing surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2} -InvalidUnicode.invalidCodePoint=Ongeldige unicode tegengekomen. Ongeldig code point. Ongeldige token ''{0}'' op lijn {1} kolom {2} -InvalidUnicode.incorrectEscape=Ongeldige unicode tegengekomen. Ongeldige geformatteerde escape-sequentie. Ongeldige token ''{0}'' op lijn {1} kolom {2}} +InvalidUnicode.trailingLeadingSurrogate=Ongeldige Unicode tegengekomen. Trailing surrogate moet vooropgaan aan een leading surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidUnicode.leadingTrailingSurrogate=Ongeldige Unicode tegengekomen. Leading surrogate moet voorafgaan aan een trailing surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidUnicode.invalidCodePoint=Ongeldige Unicode tegengekomen. Ongeldig codepunt. Ongeldige token ''{0}'' op lijn {1} kolom {2} +InvalidUnicode.incorrectEscape=Ongeldige Unicode tegengekomen. Ongeldige geformatteerde escape-sequentie. Ongeldige token ''{0}'' op lijn {1} kolom {2}} diff --git a/src/main/resources/i18n/Scalars_nl.properties b/src/main/resources/i18n/Scalars_nl.properties index 047bb692b6..f12f10e811 100644 --- a/src/main/resources/i18n/Scalars_nl.properties +++ b/src/main/resources/i18n/Scalars_nl.properties @@ -10,20 +10,20 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # -Scalar.unexpectedAstType=Verwacht werd een AST type ''{0}'', maar het was een ''{1}'' +Scalar.unexpectedAstType=Verwacht werd een AST-type ''{0}'', maar het was een ''{1}'' # Enum.badInput=Ongeldige invoer voor enum ''{0}''. Onbekende waarde ''{1}'' Enum.badName=Ongeldige invoer voor enum ''{0}''. Geen waarde gevonden voor naam ''{1}'' -Enum.unallowableValue=Literal is niet een toegestaande waarde voor enum ''{0}'' - ''{1}'' +Enum.unallowableValue=Literal is niet een toegestane waarde voor enum ''{0}'' - ''{1}'' # Int.notInt=Verwacht werd een waarde die in ''Int'' veranderd kon worden, maar het was een ''{0}'' Int.outsideRange=Verwacht werd een waarde die binnen het integerbereik valt, maar het was een ''{0}'' # ID.notId=Verwacht werd een waarde die in ''ID'' veranderd kon worden, maar het was een ''{0}'' -ID.unexpectedAstType=Verwacht werd een AST type ''IntValue'' of ''StringValue'', maar het was een ''{0}'' +ID.unexpectedAstType=Verwacht werd een AST-type ''IntValue'' of ''StringValue'', maar het was een ''{0}'' # Float.notFloat=Verwacht werd een waarde die in ''Float'' veranderd kon worden, maar het was een ''{0}'' -Float.unexpectedAstType=Verwacht werd een AST type ''IntValue'' of ''FloatValue'', maar het was een ''{0}'' +Float.unexpectedAstType=Verwacht werd een AST-type ''IntValue'' of ''FloatValue'', maar het was een ''{0}'' # Boolean.notBoolean=Verwacht werd een waarde die in ''Boolean'' veranderd kon worden, maar het was een ''{0}'' -Boolean.unexpectedAstType=Verwacht werd een AST type ''BooleanValue'', maar het was een ''{0}'' +Boolean.unexpectedAstType=Verwacht werd een AST-type ''BooleanValue'', maar het was een ''{0}'' diff --git a/src/main/resources/i18n/Validation_nl.properties b/src/main/resources/i18n/Validation_nl.properties index 5e7c4afb56..8d1a94ded6 100644 --- a/src/main/resources/i18n/Validation_nl.properties +++ b/src/main/resources/i18n/Validation_nl.properties @@ -12,7 +12,7 @@ # ExecutableDefinitions.notExecutableType=Validatiefout ({0}) : Type definitie ''{1}'' is niet uitvoerbaar ExecutableDefinitions.notExecutableSchema=Validatiefout ({0}) : Schema definitie is niet uitvoerbaar -ExecutableDefinitions.notExecutableDirective=Validatiefout ({0}) : Directive defnitie ''{1}'' is niet uitvoerbaar +ExecutableDefinitions.notExecutableDirective=Validatiefout ({0}) : Directive definitie ''{1}'' is niet uitvoerbaar ExecutableDefinitions.notExecutableDefinition=Validatiefout ({0}) : Aangeleverde definition is niet uitvoerbaar # FieldsOnCorrectType.unknownField=Validatiefout ({0}) : Veld ''{1}'' in type ''{2}'' is ongedefinieerd @@ -35,7 +35,7 @@ LoneAnonymousOperation.namedOperation=Validatiefout ({0}) : Operation ''{1}'' vo # NoFragmentCycles.cyclesNotAllowed=Validatiefout ({0}) : Fragment cycles niet toegestaan # -NoUndefinedVariables.undefinedVariable=Validatiefout ({0}) : Ongedefinieerde variable ''{1}'' +NoUndefinedVariables.undefinedVariable=Validatiefout ({0}) : Ongedefinieerde variabele ''{1}'' # NoUnusedFragments.unusedFragments=Validatiefout ({0}) : Ongebruikt fragment ''{1}'' # @@ -76,7 +76,8 @@ VariableDefaultValuesOfCorrectType.badDefault=Validatiefout ({0}) : Ongeldige st # VariablesAreInputTypes.wrongType=Validatiefout ({0}) : Invoervariabele ''{1}'' type ''{2}'' is geen invoertype # -VariableTypesMatchRule.unexpectedType=Validatiefout ({0}) : Variable type ''{1}'' komt niet overeen met het verwachte type ''{2}'' +VariableTypesMatchRule.unexpectedType=Validatiefout ({0}) : Variabele type ''{1}'' komt niet overeen met het verwachte type ''{2}'' +UniqueObjectFieldName.duplicateFieldName=Validatiefout ({0}) : Er kan slechts één veld genaamd ''{1}'' zijn # # These are used but IDEA cant find them easily as being called # From 6959620c885fedd14855dcd96a9ad4e452cf771d Mon Sep 17 00:00:00 2001 From: Jordie Biemold Date: Fri, 27 Oct 2023 20:24:23 +0200 Subject: [PATCH 015/393] Fix encoding of Dutch translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Important for één (one) --- src/main/resources/i18n/Execution_nl.properties | 2 +- src/main/resources/i18n/Parsing_nl.properties | 1 - src/main/resources/i18n/Validation_nl.properties | 14 +++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/resources/i18n/Execution_nl.properties b/src/main/resources/i18n/Execution_nl.properties index f544873152..eee9f69ea0 100644 --- a/src/main/resources/i18n/Execution_nl.properties +++ b/src/main/resources/i18n/Execution_nl.properties @@ -4,5 +4,5 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # -Execution.handleOneOfNotOneFieldError=Er moet exact één sleutel aangegeven worden voor OneOf type ''{0}''. +Execution.handleOneOfNotOneFieldError=Er moet exact één sleutel aangegeven worden voor OneOf type ''{0}''. Execution.handleOneOfValueIsNullError=OneOf type field ''{0}'' mag niet null zijn. diff --git a/src/main/resources/i18n/Parsing_nl.properties b/src/main/resources/i18n/Parsing_nl.properties index 830bae5f6a..dfa099a91a 100644 --- a/src/main/resources/i18n/Parsing_nl.properties +++ b/src/main/resources/i18n/Parsing_nl.properties @@ -12,7 +12,6 @@ # InvalidSyntax.noMessage=Ongeldige syntaxis op lijn {0} kolom {1} InvalidSyntax.full=Ongeldige syntaxis, ANTLR foutmelding ''{0}'' op lijn {1} kolom {2} - InvalidSyntaxBail.noToken=Ongeldige syntaxis op lijn {0} kolom {1} InvalidSyntaxBail.full=Ongeldige syntaxis wegens ongeldige token ''{0}'' op lijn {1} kolom {2} # diff --git a/src/main/resources/i18n/Validation_nl.properties b/src/main/resources/i18n/Validation_nl.properties index 8d1a94ded6..1b5dc745ce 100644 --- a/src/main/resources/i18n/Validation_nl.properties +++ b/src/main/resources/i18n/Validation_nl.properties @@ -57,27 +57,27 @@ ProvidedNonNullArguments.nullValue=Validatiefout ({0}) : Null waarde voor non-nu ScalarLeaves.subselectionOnLeaf=Validatiefout ({0}) : Selectie niet toegestaan op leaf type ''{1}'' van veld ''{2}'' ScalarLeaves.subselectionRequired=Validatiefout ({0}) : Selectie verplicht voor type ''{1}'' van veld ''{2}'' # -SubscriptionUniqueRootField.multipleRootFields=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field hebben -SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field met fragmenten hebben +SubscriptionUniqueRootField.multipleRootFields=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field hebben +SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field met fragmenten hebben SubscriptionIntrospectionRootField.introspectionRootField=Validatiefout ({0}) : Subscription operation ''{1}'' root field ''{2}'' kan geen introspectieveld zijn SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' kan geen introspectieveld zijn # -UniqueArgumentNames.uniqueArgument=Validatiefout ({0}) : Er mag maar één argument met naam ''{1}'' bestaan +UniqueArgumentNames.uniqueArgument=Validatiefout ({0}) : Er mag maar één argument met naam ''{1}'' bestaan # UniqueDirectiveNamesPerLocation.uniqueDirectives=Validatiefout ({0}) : Onherhaalbare directives moeten een unieke naam hebben binnen een locatie. De directive ''{1}'' gebruikt op een ''{2}'' is niet uniek # -UniqueFragmentNames.oneFragment=Validatiefout ({0}) : Er mag maar één fragment met naam ''{1}'' bestaan +UniqueFragmentNames.oneFragment=Validatiefout ({0}) : Er mag maar één fragment met naam ''{1}'' bestaan # -UniqueOperationNames.oneOperation=Validatiefout ({0}) : Er mag maar één operatie met naam ''{1}'' bestaan +UniqueOperationNames.oneOperation=Validatiefout ({0}) : Er mag maar één operatie met naam ''{1}'' bestaan # -UniqueVariableNames.oneVariable=Validatiefout ({0}) : Er mag maar één variabele met naam ''{1}'' bestaan +UniqueVariableNames.oneVariable=Validatiefout ({0}) : Er mag maar één variabele met naam ''{1}'' bestaan # VariableDefaultValuesOfCorrectType.badDefault=Validatiefout ({0}) : Ongeldige standaardwaarde ''{1}'' voor type ''{2}'' # VariablesAreInputTypes.wrongType=Validatiefout ({0}) : Invoervariabele ''{1}'' type ''{2}'' is geen invoertype # VariableTypesMatchRule.unexpectedType=Validatiefout ({0}) : Variabele type ''{1}'' komt niet overeen met het verwachte type ''{2}'' -UniqueObjectFieldName.duplicateFieldName=Validatiefout ({0}) : Er kan slechts één veld genaamd ''{1}'' zijn +UniqueObjectFieldName.duplicateFieldName=Validatiefout ({0}) : Er kan slechts één veld genaamd ''{1}'' zijn # # These are used but IDEA cant find them easily as being called # From 5aabc7c3f9328d2a078f78f062f6c53d8174807e Mon Sep 17 00:00:00 2001 From: Jordie Biemold Date: Fri, 27 Oct 2023 20:26:46 +0200 Subject: [PATCH 016/393] Fix last spelling mistakes in NL translation --- src/main/resources/i18n/Validation_nl.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/i18n/Validation_nl.properties b/src/main/resources/i18n/Validation_nl.properties index 1b5dc745ce..e30b342640 100644 --- a/src/main/resources/i18n/Validation_nl.properties +++ b/src/main/resources/i18n/Validation_nl.properties @@ -54,8 +54,8 @@ ProvidedNonNullArguments.missingFieldArg=Validatiefout ({0}) : Missend field arg ProvidedNonNullArguments.missingDirectiveArg=Validatiefout ({0}) : Missend directive argument ''{1}'' ProvidedNonNullArguments.nullValue=Validatiefout ({0}) : Null waarde voor non-null field argument ''{1}'' # -ScalarLeaves.subselectionOnLeaf=Validatiefout ({0}) : Selectie niet toegestaan op leaf type ''{1}'' van veld ''{2}'' -ScalarLeaves.subselectionRequired=Validatiefout ({0}) : Selectie verplicht voor type ''{1}'' van veld ''{2}'' +ScalarLeaves.subselectionOnLeaf=Validatiefout ({0}) : Sub-selectie niet toegestaan op leaf/uiteinde type ''{1}'' van veld ''{2}'' +ScalarLeaves.subselectionRequired=Validatiefout ({0}) : Sub-selectie verplicht voor type ''{1}'' van veld ''{2}'' # SubscriptionUniqueRootField.multipleRootFields=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field hebben SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field met fragmenten hebben @@ -96,5 +96,5 @@ ArgumentValidationUtil.handleNotObjectError=Validatiefout ({0}) : argument ''{1} # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleMissingFieldsError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' mist een verplicht veld ''{3}'' # suppress inspection "UnusedProperty" -ArgumentValidationUtil.handleExtraFieldError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' bevalt een veld niet in ''{3}'': ''{4}'' +ArgumentValidationUtil.handleExtraFieldError=Validatiefout ({0}) : argument ''{1}'' met waarde ''{2}'' bevat een veld niet in ''{3}'': ''{4}'' # From 0374d4ec30f67bd3050a44188790a4e79a25586f Mon Sep 17 00:00:00 2001 From: Jordie Biemold Date: Fri, 27 Oct 2023 20:31:31 +0200 Subject: [PATCH 017/393] Fix DE i18n encoding --- .../resources/i18n/Execution_de.properties | 2 +- src/main/resources/i18n/Parsing_de.properties | 22 +++++------ src/main/resources/i18n/Scalars_de.properties | 6 +-- .../resources/i18n/Validation_de.properties | 38 +++++++++---------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/main/resources/i18n/Execution_de.properties b/src/main/resources/i18n/Execution_de.properties index aff0b11d00..e594eb287a 100644 --- a/src/main/resources/i18n/Execution_de.properties +++ b/src/main/resources/i18n/Execution_de.properties @@ -4,5 +4,5 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # -Execution.handleOneOfNotOneFieldError=Es muss genau ein Schlüssel angegeben werden für OneOf Typ ''{0}''. +Execution.handleOneOfNotOneFieldError=Es muss genau ein Schlüssel angegeben werden für OneOf Typ ''{0}''. Execution.handleOneOfValueIsNullError=OneOf type field ''{0}'' darf nicht null sein. diff --git a/src/main/resources/i18n/Parsing_de.properties b/src/main/resources/i18n/Parsing_de.properties index 9877badacf..2d3c43f777 100644 --- a/src/main/resources/i18n/Parsing_de.properties +++ b/src/main/resources/i18n/Parsing_de.properties @@ -10,19 +10,19 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # -InvalidSyntax.noMessage=Ungültige Syntax in Zeile {0} Spalte {1} -InvalidSyntax.full=Ungültige Syntax, ANTLR-Fehler ''{0}'' in Zeile {1} Spalte {2} +InvalidSyntax.noMessage=Ungültige Syntax in Zeile {0} Spalte {1} +InvalidSyntax.full=Ungültige Syntax, ANTLR-Fehler ''{0}'' in Zeile {1} Spalte {2} -InvalidSyntaxBail.noToken=Ungültige Syntax in Zeile {0} Spalte {1} -InvalidSyntaxBail.full=Ungültige Syntax wegen des ungültigen Tokens ''{0}'' in Zeile {1} Spalte {2} +InvalidSyntaxBail.noToken=Ungültige Syntax in Zeile {0} Spalte {1} +InvalidSyntaxBail.full=Ungültige Syntax wegen des ungültigen Tokens ''{0}'' in Zeile {1} Spalte {2} # -InvalidSyntaxMoreTokens.full=Es wurde eine ungültige Syntax festgestellt. Es gibt zusätzliche Token im Text, die nicht konsumiert wurden. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidSyntaxMoreTokens.full=Es wurde eine ungültige Syntax festgestellt. Es gibt zusätzliche Token im Text, die nicht konsumiert wurden. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} # -ParseCancelled.full=Es wurden mehr als {0} ''{1}'' Token präsentiert. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. -ParseCancelled.tooDeep=Es wurden mehr als {0} tief ''{1}'' Regeln ausgeführt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. +ParseCancelled.full=Es wurden mehr als {0} ''{1}'' Token präsentiert. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. +ParseCancelled.tooDeep=Es wurden mehr als {0} tief ''{1}'' Regeln ausgeführt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. ParseCancelled.tooManyChars=Es wurden mehr als {0} Zeichen vorgelegt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen. # -InvalidUnicode.trailingLeadingSurrogate=Ungültiger Unicode gefunden. Trailing surrogate muss ein leading surrogate vorangestellt werden. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} -InvalidUnicode.leadingTrailingSurrogate=Ungültiger Unicode gefunden. Auf ein leading surrogate muss ein trailing surrogate folgen. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} -InvalidUnicode.invalidCodePoint=Ungültiger Unicode gefunden. Kein gültiger code point. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} -InvalidUnicode.incorrectEscape=Ungültiger Unicode gefunden. Falsch formatierte Escape-Sequenz. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidUnicode.trailingLeadingSurrogate=Ungültiger Unicode gefunden. Trailing surrogate muss ein leading surrogate vorangestellt werden. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidUnicode.leadingTrailingSurrogate=Ungültiger Unicode gefunden. Auf ein leading surrogate muss ein trailing surrogate folgen. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidUnicode.invalidCodePoint=Ungültiger Unicode gefunden. Kein gültiger code point. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidUnicode.incorrectEscape=Ungültiger Unicode gefunden. Falsch formatierte Escape-Sequenz. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2} diff --git a/src/main/resources/i18n/Scalars_de.properties b/src/main/resources/i18n/Scalars_de.properties index 645ed5e7ed..cb06a23669 100644 --- a/src/main/resources/i18n/Scalars_de.properties +++ b/src/main/resources/i18n/Scalars_de.properties @@ -12,9 +12,9 @@ # Scalar.unexpectedAstType=Erwartet wurde ein AST type von ''{0}'', aber es war ein ''{1}'' # -Enum.badInput=Ungültige Eingabe für enum ''{0}''. Unbekannter Wert ''{1}'' -Enum.badName=Ungültige Eingabe für enum ''{0}''. Kein Wert für den Namen ''{1}'' gefunden -Enum.unallowableValue=Literal nicht in den zulässigen Werten für enum ''{0}'' - ''{1}'' +Enum.badInput=Ungültige Eingabe für enum ''{0}''. Unbekannter Wert ''{1}'' +Enum.badName=Ungültige Eingabe für enum ''{0}''. Kein Wert für den Namen ''{1}'' gefunden +Enum.unallowableValue=Literal nicht in den zulässigen Werten für enum ''{0}'' - ''{1}'' # Int.notInt=Erwartet wurde ein Wert, der in den Typ ''Int'' konvertiert werden kann, aber es war ein ''{0}'' Int.outsideRange=Erwarteter Wert im Integer-Bereich, aber es war ein ''{0}'' diff --git a/src/main/resources/i18n/Validation_de.properties b/src/main/resources/i18n/Validation_de.properties index 41ddd049a5..fec637643c 100644 --- a/src/main/resources/i18n/Validation_de.properties +++ b/src/main/resources/i18n/Validation_de.properties @@ -10,15 +10,15 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # -ExecutableDefinitions.notExecutableType=Validierungsfehler ({0}) : Type definition ''{1}'' ist nicht ausführbar -ExecutableDefinitions.notExecutableSchema=Validierungsfehler ({0}) : Schema definition ist nicht ausführbar -ExecutableDefinitions.notExecutableDirective=Validierungsfehler ({0}) : Directive definition ''{1}'' ist nicht ausführbar -ExecutableDefinitions.notExecutableDefinition=Validierungsfehler ({0}) : Die angegebene Definition ist nicht ausführbar +ExecutableDefinitions.notExecutableType=Validierungsfehler ({0}) : Type definition ''{1}'' ist nicht ausführbar +ExecutableDefinitions.notExecutableSchema=Validierungsfehler ({0}) : Schema definition ist nicht ausführbar +ExecutableDefinitions.notExecutableDirective=Validierungsfehler ({0}) : Directive definition ''{1}'' ist nicht ausführbar +ExecutableDefinitions.notExecutableDefinition=Validierungsfehler ({0}) : Die angegebene Definition ist nicht ausführbar # FieldsOnCorrectType.unknownField=Validierungsfehler ({0}) : Feld ''{1}'' vom Typ ''{2}'' ist nicht definiert # -FragmentsOnCompositeType.invalidInlineTypeCondition=Validierungsfehler ({0}) : Inline fragment type condition ist ungültig, muss auf Object/Interface/Union stehen -FragmentsOnCompositeType.invalidFragmentTypeCondition=Validierungsfehler ({0}) : Fragment type condition ist ungültig, muss auf Object/Interface/Union stehen +FragmentsOnCompositeType.invalidInlineTypeCondition=Validierungsfehler ({0}) : Inline fragment type condition ist ungültig, muss auf Object/Interface/Union stehen +FragmentsOnCompositeType.invalidFragmentTypeCondition=Validierungsfehler ({0}) : Fragment type condition ist ungültig, muss auf Object/Interface/Union stehen # KnownArgumentNames.unknownDirectiveArg=Validierungsfehler ({0}) : Unbekanntes directive argument ''{1}'' KnownArgumentNames.unknownFieldArg=Validierungsfehler ({0}) : Unbekanntes field argument ''{1}'' @@ -45,17 +45,17 @@ OverlappingFieldsCanBeMerged.differentFields=Validierungsfehler ({0}) : ''{1}'' OverlappingFieldsCanBeMerged.differentArgs=Validierungsfehler ({0}) : ''{1}'' : Felder haben unterschiedliche Argumente OverlappingFieldsCanBeMerged.differentNullability=Validierungsfehler ({0}) : ''{1}'' : Felder haben unterschiedliche nullability shapes OverlappingFieldsCanBeMerged.differentLists=Validierungsfehler ({0}) : ''{1}'' : Felder haben unterschiedliche list shapes -OverlappingFieldsCanBeMerged.differentReturnTypes=Validierungsfehler ({0}) : ''{1}'' : gibt verschiedene Typen ''{2}'' und ''{3}'' zurück +OverlappingFieldsCanBeMerged.differentReturnTypes=Validierungsfehler ({0}) : ''{1}'' : gibt verschiedene Typen ''{2}'' und ''{3}'' zurück # -PossibleFragmentSpreads.inlineIncompatibleTypes=Validierungsfehler ({0}) : Fragment kann hier nicht verbreitet werden, da object vom Typ ''{1}'' niemals vom Typ ''{2}'' sein können -PossibleFragmentSpreads.fragmentIncompatibleTypes=Validierungsfehler ({0}) : Fragment ''{1}'' kann hier nicht verbreitet werden, da object vom Typ ''{2}'' niemals vom Typ ''{3}'' sein können +PossibleFragmentSpreads.inlineIncompatibleTypes=Validierungsfehler ({0}) : Fragment kann hier nicht verbreitet werden, da object vom Typ ''{1}'' niemals vom Typ ''{2}'' sein können +PossibleFragmentSpreads.fragmentIncompatibleTypes=Validierungsfehler ({0}) : Fragment ''{1}'' kann hier nicht verbreitet werden, da object vom Typ ''{2}'' niemals vom Typ ''{3}'' sein können # ProvidedNonNullArguments.missingFieldArg=Validierungsfehler ({0}) : Fehlendes field argument ''{1}'' ProvidedNonNullArguments.missingDirectiveArg=Validierungsfehler ({0}) : Fehlendes directive argument ''{1}'' -ProvidedNonNullArguments.nullValue=Validierungsfehler ({0}) : Nullwert für non-null field argument ''{1}'' +ProvidedNonNullArguments.nullValue=Validierungsfehler ({0}) : Nullwert für non-null field argument ''{1}'' # -ScalarLeaves.subselectionOnLeaf=Validierungsfehler ({0}) : Unterauswahl für Blatttyp ''{1}'' von Feld ''{2}'' nicht zulässig -ScalarLeaves.subselectionRequired=Validierungsfehler ({0}) : Unterauswahl erforderlich für Typ ''{1}'' des Feldes ''{2}'' +ScalarLeaves.subselectionOnLeaf=Validierungsfehler ({0}) : Unterauswahl für Blatttyp ''{1}'' von Feld ''{2}'' nicht zulässig +ScalarLeaves.subselectionRequired=Validierungsfehler ({0}) : Unterauswahl erforderlich für Typ ''{1}'' des Feldes ''{2}'' # SubscriptionUniqueRootField.multipleRootFields=Validierungsfehler ({0}) : Subscription operation ''{1}'' muss genau ein root field haben SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validierungsfehler ({0}) : Subscription operation ''{1}'' muss genau ein root field mit Fragmenten haben @@ -64,7 +64,7 @@ SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validierun # UniqueArgumentNames.uniqueArgument=Validierungsfehler ({0}) : Es kann nur ein Argument namens ''{1}'' geben # -UniqueDirectiveNamesPerLocation.uniqueDirectives=Validierungsfehler ({0}) : Nicht wiederholbare directive müssen innerhalb einer Lokation eindeutig benannt werden. Directive ''{1}'', die auf einem ''{2}'' verwendet wird, ist nicht eindeutig +UniqueDirectiveNamesPerLocation.uniqueDirectives=Validierungsfehler ({0}) : Nicht wiederholbare directive müssen innerhalb einer Lokation eindeutig benannt werden. Directive ''{1}'', die auf einem ''{2}'' verwendet wird, ist nicht eindeutig # UniqueFragmentNames.oneFragment=Validierungsfehler ({0}) : Es kann nur ein Fragment namens ''{1}'' geben # @@ -72,7 +72,7 @@ UniqueOperationNames.oneOperation=Validierungsfehler ({0}) : Es kann nur eine Op # UniqueVariableNames.oneVariable=Validierungsfehler ({0}) : Es kann nur eine Variable namens ''{1}'' geben # -VariableDefaultValuesOfCorrectType.badDefault=Validierungsfehler ({0}) : Ungültiger Standardwert ''{1}'' für Typ ''{2}'' +VariableDefaultValuesOfCorrectType.badDefault=Validierungsfehler ({0}) : Ungültiger Standardwert ''{1}'' für Typ ''{2}'' # VariablesAreInputTypes.wrongType=Validierungsfehler ({0}) : Eingabevariable ''{1}'' Typ ''{2}'' ist kein Eingabetyp # @@ -84,17 +84,17 @@ UniqueObjectFieldName.duplicateFieldName=Validierungsfehler ({0}) : Es kann nur # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleNullError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' darf nicht null sein # suppress inspection "UnusedProperty" -ArgumentValidationUtil.handleScalarError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' +ArgumentValidationUtil.handleScalarError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' # suppress inspection "UnusedProperty" -ArgumentValidationUtil.handleScalarErrorCustomMessage=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' - {4} +ArgumentValidationUtil.handleScalarErrorCustomMessage=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' - {4} # suppress inspection "UnusedProperty" -ArgumentValidationUtil.handleEnumError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' +ArgumentValidationUtil.handleEnumError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' # suppress inspection "UnusedProperty" -ArgumentValidationUtil.handleEnumErrorCustomMessage=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' - {4} +ArgumentValidationUtil.handleEnumErrorCustomMessage=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein gültiges ''{3}'' - {4} # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleNotObjectError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' muss ein object type sein # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleMissingFieldsError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' fehlen Pflichtfelder ''{3}'' # suppress inspection "UnusedProperty" -ArgumentValidationUtil.handleExtraFieldError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' enthält ein Feld nicht in ''{3}'': ''{4}'' +ArgumentValidationUtil.handleExtraFieldError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' enthält ein Feld nicht in ''{3}'': ''{4}'' # From 904fd1e12afe00870e0bb372c52843e8909af5b5 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 30 Oct 2023 08:40:52 +1100 Subject: [PATCH 018/393] Add @Internal to new class --- .../java/graphql/execution/ValuesResolverOneOfValidation.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index a024e44307..bbe3ed79e7 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -1,6 +1,7 @@ package graphql.execution; import graphql.Assert; +import graphql.Internal; import graphql.i18n.I18n; import graphql.language.ObjectField; import graphql.language.ObjectValue; @@ -16,6 +17,7 @@ import java.util.Map; import java.util.stream.Collectors; +@Internal final class ValuesResolverOneOfValidation { @SuppressWarnings("unchecked") From 364c6770105dca01f3386abe52ea9f5426bf8e0b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 30 Oct 2023 19:56:00 +1100 Subject: [PATCH 019/393] Test showing lists not handled for oneOf --- .../execution/ValuesResolverTest.groovy | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 92cd2b1ce7..df67ef28d5 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -603,6 +603,40 @@ class ValuesResolverTest extends Specification { } + def "getArgumentValues: invalid oneOf input validation applied with list of input type"() { + given: "schema defining input object" + def inputObjectType = newInputObject() + .name("oneOfInputObject") + .withAppliedDirective(Directives.OneOfDirective.toAppliedDirective()) + .field(newInputObjectField() + .name("a") + .type(GraphQLString) + .build()) + .field(newInputObjectField() + .name("b") + .type(GraphQLInt) + .build()) + .build() + + def inputValue = buildObjectLiteral([ + oneOfField: [ + a: StringValue.of("abc"), + b: IntValue.of(123) + ] + ]) + def inputArray = ArrayValue.newArrayValue().value(inputValue).build() + + def argument = new Argument("arg", inputArray) + + when: + def fieldArgumentList = newArgument().name("arg").type(list(inputObjectType)).build() + ValuesResolver.getArgumentValues([fieldArgumentList], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) + + then: + def e = thrown(OneOfTooManyKeysException) + e.message == "Exactly one key must be specified for OneOf type 'oneOfInputObject'." + } + def "getArgumentValues: valid oneOf input - #testCase"() { given: "schema defining input object" def inputObjectType = newInputObject() From f7dedcc13de8272235cacd6c050c3d493c0c635d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:12:32 +0000 Subject: [PATCH 020/393] Bump me.champeau.jmh from 0.7.1 to 0.7.2 Bumps me.champeau.jmh from 0.7.1 to 0.7.2. --- updated-dependencies: - dependency-name: me.champeau.jmh dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e416a40faa..0f88a5f80a 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { id "biz.aQute.bnd.builder" version "6.4.0" id "io.github.gradle-nexus.publish-plugin" version "1.3.0" id "groovy" - id "me.champeau.jmh" version "0.7.1" + id "me.champeau.jmh" version "0.7.2" } java { From 9156cd06a1632fc3d139a42ce8e86666fec0b7b4 Mon Sep 17 00:00:00 2001 From: Salvo Miosi Date: Mon, 30 Oct 2023 19:24:45 +0100 Subject: [PATCH 021/393] Add tests for compute methods --- .../groovy/graphql/GraphQLContextTest.groovy | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/test/groovy/graphql/GraphQLContextTest.groovy b/src/test/groovy/graphql/GraphQLContextTest.groovy index f409721363..8eebb17653 100644 --- a/src/test/groovy/graphql/GraphQLContextTest.groovy +++ b/src/test/groovy/graphql/GraphQLContextTest.groovy @@ -168,6 +168,52 @@ class GraphQLContextTest extends Specification { !context.hasKey("k3") } + def "compute works"() { + def context + when: + context = buildContext([k1: "foo"]) + then: + context.compute("k1", (k, v) -> v ? v + "bar" : "default") == "foobar" + context.get("k1") == "foobar" + context.compute("k2", (k, v) -> v ? "new" : "default") == "default" + context.get("k2") == "default" + !context.compute("k3", (k, v) -> null) + !context.hasKey("k3") + sizeOf(context) == 2 + } + + def "computeIfAbsent works"() { + def context + when: + context = buildContext([k1: "v1", k2: "v2"]) + then: + context.computeIfAbsent("k1", k -> "default") == "v1" + context.get("k1") == "v1" + context.computeIfAbsent("k2", k -> null) == "v2" + context.get("k2") == "v2" + context.computeIfAbsent("k3", k -> "default") == "default" + context.get("k3") == "default" + !context.computeIfAbsent("k4", k -> null) + !context.hasKey("k4") + sizeOf(context) == 3 + } + + def "computeIfPresent works"() { + def context + when: + context = buildContext([k1: "foo", k2: "v2"]) + then: + context.computeIfPresent("k1", (k, v) -> v + "bar") == "foobar" + context.get("k1") == "foobar" + !context.computeIfPresent("k2", (k, v) -> null) + !context.hasKey("k2") + !context.computeIfPresent("k3", (k, v) -> v + "bar") + !context.hasKey("k3") + !context.computeIfPresent("k4", (k, v) -> null) + !context.hasKey("k4") + sizeOf(context) == 1 + } + def "getOrDefault works"() { def context when: From 820516708974a5d12c1d2d7816e6db4a8aaddbac Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:06:58 +1100 Subject: [PATCH 022/393] Add oneOf validation for lists --- .../graphql/execution/ValuesResolver.java | 1 - .../ValuesResolverOneOfValidation.java | 23 +- .../execution/ValuesResolverTest.groovy | 226 +++++++++++++++++- 3 files changed, 234 insertions(+), 16 deletions(-) diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index e269a63b4c..38ea753f37 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -27,7 +27,6 @@ import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; import graphql.schema.InputValueWithState; import graphql.schema.visibility.GraphqlFieldVisibility; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index bbe3ed79e7..6456ffaa01 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -3,12 +3,14 @@ import graphql.Assert; import graphql.Internal; import graphql.i18n.I18n; +import graphql.language.ArrayValue; import graphql.language.ObjectField; import graphql.language.ObjectValue; import graphql.language.Value; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLList; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; @@ -17,18 +19,33 @@ import java.util.Map; import java.util.stream.Collectors; +import static graphql.schema.GraphQLTypeUtil.isList; + @Internal final class ValuesResolverOneOfValidation { @SuppressWarnings("unchecked") static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value argumentValue, String argumentName, Locale locale) { - GraphQLType unwrappedType = GraphQLTypeUtil.unwrapNonNull(type); + GraphQLType unwrappedNonNullType = GraphQLTypeUtil.unwrapNonNull(type); + + if (isList(unwrappedNonNullType) + && !ValuesResolverConversion.isNullValue(inputValue) + && inputValue instanceof List + && argumentValue instanceof ArrayValue) { + GraphQLType elementType = ((GraphQLList) unwrappedNonNullType).getWrappedType(); + List inputList = (List) inputValue; + List argumentList = ((ArrayValue) argumentValue).getValues(); + + for (int i = 0; i < argumentList.size(); i++) { + validateOneOfInputTypes(elementType, inputList.get(i), argumentList.get(i), argumentName, locale); + } + } - if (unwrappedType instanceof GraphQLInputObjectType && !ValuesResolverConversion.isNullValue(inputValue)) { + if (unwrappedNonNullType instanceof GraphQLInputObjectType && !ValuesResolverConversion.isNullValue(inputValue)) { Assert.assertTrue(inputValue instanceof Map, () -> String.format("The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName)); Map objectMap = (Map) inputValue; - GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedType; + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedNonNullType; if (inputObjectType.isOneOf()) { validateOneOfInputTypesInternal(inputObjectType, argumentValue, objectMap, locale); diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index df67ef28d5..c963ab8e3c 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -23,7 +23,6 @@ import graphql.language.Value import graphql.language.VariableDefinition import graphql.language.VariableReference import graphql.schema.CoercingParseValueException -import graphql.schema.GraphQLNonNull import spock.lang.Specification import spock.lang.Unroll @@ -373,7 +372,7 @@ class ValuesResolverTest extends Specification { e.message == "Exactly one key must be specified for OneOf type 'oneOfInputObject'." when: "input type is wrapped in non-null" - def nonNullInputObjectType = GraphQLNonNull.nonNull(inputObjectType) + def nonNullInputObjectType = nonNull(inputObjectType) def fieldArgumentNonNull = newArgument().name("arg").type(nonNullInputObjectType).build() ValuesResolver.getArgumentValues([fieldArgumentNonNull], [argument], variables, graphQLContext, locale) @@ -603,7 +602,7 @@ class ValuesResolverTest extends Specification { } - def "getArgumentValues: invalid oneOf input validation applied with list of input type"() { + def "getArgumentValues: invalid oneOf list input because element contains duplicate key - #testCase"() { given: "schema defining input object" def inputObjectType = newInputObject() .name("oneOfInputObject") @@ -618,23 +617,226 @@ class ValuesResolverTest extends Specification { .build()) .build() - def inputValue = buildObjectLiteral([ - oneOfField: [ - a: StringValue.of("abc"), - b: IntValue.of(123) - ] - ]) - def inputArray = ArrayValue.newArrayValue().value(inputValue).build() - + when: def argument = new Argument("arg", inputArray) + def fieldArgumentList = newArgument().name("arg").type(list(inputObjectType)).build() + ValuesResolver.getArgumentValues([fieldArgumentList], [argument], variables, graphQLContext, locale) + + then: + def e = thrown(OneOfTooManyKeysException) + e.message == "Exactly one key must be specified for OneOf type 'oneOfInputObject'." + + where: + + testCase | inputArray | variables + + '[{ a: "abc", b: 123 }]' + | ArrayValue.newArrayValue() + .value(buildObjectLiteral([ + a: StringValue.of("abc"), + b: IntValue.of(123) + ])).build() + | CoercedVariables.emptyVariables() + + '[{ a: "abc" }, { a: "xyz", b: 789 }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + buildObjectLiteral([ + a: StringValue.of("xyz"), + b: IntValue.of(789) + ]), + ]).build() + | CoercedVariables.emptyVariables() + + '[{ a: "abc" }, $var ] [{ a: "abc" }, { a: "xyz", b: 789 }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + VariableReference.of("var") + ]).build() + | CoercedVariables.of("var": [a: "xyz", b: 789]) + + } + + def "getArgumentValues: invalid oneOf list input because element contains null value - #testCase"() { + given: "schema defining input object" + def inputObjectType = newInputObject() + .name("oneOfInputObject") + .withAppliedDirective(Directives.OneOfDirective.toAppliedDirective()) + .field(newInputObjectField() + .name("a") + .type(GraphQLString) + .build()) + .field(newInputObjectField() + .name("b") + .type(GraphQLInt) + .build()) + .build() when: + def argument = new Argument("arg", inputArray) def fieldArgumentList = newArgument().name("arg").type(list(inputObjectType)).build() - ValuesResolver.getArgumentValues([fieldArgumentList], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) + ValuesResolver.getArgumentValues([fieldArgumentList], [argument], variables, graphQLContext, locale) + + then: + def e = thrown(OneOfNullValueException) + e.message == "OneOf type field 'oneOfInputObject.a' must be non-null." + + where: + + testCase | inputArray | variables + + '[{ a: "abc" }, { a: null }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + buildObjectLiteral([ + a: NullValue.of() + ]), + ]).build() + | CoercedVariables.emptyVariables() + + '[{ a: "abc" }, { a: $var }] [{ a: "abc" }, { a: null }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + buildObjectLiteral([ + a: VariableReference.of("var") + ]), + ]).build() + | CoercedVariables.of("var": null) + + } + + def "getArgumentValues: invalid oneOf non-null list input because element contains duplicate key - #testCase"() { + given: "schema defining input object" + def inputObjectType = newInputObject() + .name("oneOfInputObject") + .withAppliedDirective(Directives.OneOfDirective.toAppliedDirective()) + .field(newInputObjectField() + .name("a") + .type(GraphQLString) + .build()) + .field(newInputObjectField() + .name("b") + .type(GraphQLInt) + .build()) + .build() + + when: + def argument = new Argument("arg", inputArray) + def fieldArgumentList = newArgument().name("arg").type(nonNull(list(inputObjectType))).build() + ValuesResolver.getArgumentValues([fieldArgumentList], [argument], variables, graphQLContext, locale) then: def e = thrown(OneOfTooManyKeysException) e.message == "Exactly one key must be specified for OneOf type 'oneOfInputObject'." + + where: + + testCase | inputArray | variables + + '[{ a: "abc", b: 123 }]' + | ArrayValue.newArrayValue() + .value(buildObjectLiteral([ + a: StringValue.of("abc"), + b: IntValue.of(123) + ])).build() + | CoercedVariables.emptyVariables() + + '[{ a: "abc" }, { a: "xyz", b: 789 }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + buildObjectLiteral([ + a: StringValue.of("xyz"), + b: IntValue.of(789) + ]), + ]).build() + | CoercedVariables.emptyVariables() + + '[{ a: "abc" }, $var ] [{ a: "abc" }, { a: "xyz", b: 789 }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + VariableReference.of("var") + ]).build() + | CoercedVariables.of("var": [a: "xyz", b: 789]) + + } + + def "getArgumentValues: invalid oneOf list input with non-nullable elements, because element contains duplicate key - #testCase"() { + given: "schema defining input object" + def inputObjectType = newInputObject() + .name("oneOfInputObject") + .withAppliedDirective(Directives.OneOfDirective.toAppliedDirective()) + .field(newInputObjectField() + .name("a") + .type(GraphQLString) + .build()) + .field(newInputObjectField() + .name("b") + .type(GraphQLInt) + .build()) + .build() + + when: + def argument = new Argument("arg", inputArray) + def fieldArgumentList = newArgument().name("arg").type(list(nonNull(inputObjectType))).build() + ValuesResolver.getArgumentValues([fieldArgumentList], [argument], variables, graphQLContext, locale) + + then: + def e = thrown(OneOfTooManyKeysException) + e.message == "Exactly one key must be specified for OneOf type 'oneOfInputObject'." + + where: + + testCase | inputArray | variables + + '[{ a: "abc", b: 123 }]' + | ArrayValue.newArrayValue() + .value(buildObjectLiteral([ + a: StringValue.of("abc"), + b: IntValue.of(123) + ])).build() + | CoercedVariables.emptyVariables() + + '[{ a: "abc" }, { a: "xyz", b: 789 }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + buildObjectLiteral([ + a: StringValue.of("xyz"), + b: IntValue.of(789) + ]), + ]).build() + | CoercedVariables.emptyVariables() + + '[{ a: "abc" }, $var ] [{ a: "abc" }, { a: "xyz", b: 789 }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + VariableReference.of("var") + ]).build() + | CoercedVariables.of("var": [a: "xyz", b: 789]) + } def "getArgumentValues: valid oneOf input - #testCase"() { From 09d61f4bd6cdb2d0008fc6344814d05a90c21ce8 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:11:21 +1100 Subject: [PATCH 023/393] Tidy up --- .../execution/ValuesResolverTest.groovy | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index c963ab8e3c..8f46042922 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -645,8 +645,8 @@ class ValuesResolverTest extends Specification { a: StringValue.of("abc") ]), buildObjectLiteral([ - a: StringValue.of("xyz"), - b: IntValue.of(789) + a: StringValue.of("xyz"), + b: IntValue.of(789) ]), ]).build() | CoercedVariables.emptyVariables() @@ -655,7 +655,7 @@ class ValuesResolverTest extends Specification { | ArrayValue.newArrayValue() .values([ buildObjectLiteral([ - a: StringValue.of("abc") + a: StringValue.of("abc") ]), VariableReference.of("var") ]).build() @@ -695,10 +695,10 @@ class ValuesResolverTest extends Specification { | ArrayValue.newArrayValue() .values([ buildObjectLiteral([ - a: StringValue.of("abc") + a: StringValue.of("abc") ]), buildObjectLiteral([ - a: NullValue.of() + a: NullValue.of() ]), ]).build() | CoercedVariables.emptyVariables() @@ -707,10 +707,10 @@ class ValuesResolverTest extends Specification { | ArrayValue.newArrayValue() .values([ buildObjectLiteral([ - a: StringValue.of("abc") + a: StringValue.of("abc") ]), buildObjectLiteral([ - a: VariableReference.of("var") + a: VariableReference.of("var") ]), ]).build() | CoercedVariables.of("var": null) @@ -757,11 +757,11 @@ class ValuesResolverTest extends Specification { | ArrayValue.newArrayValue() .values([ buildObjectLiteral([ - a: StringValue.of("abc") + a: StringValue.of("abc") ]), buildObjectLiteral([ - a: StringValue.of("xyz"), - b: IntValue.of(789) + a: StringValue.of("xyz"), + b: IntValue.of(789) ]), ]).build() | CoercedVariables.emptyVariables() @@ -770,7 +770,7 @@ class ValuesResolverTest extends Specification { | ArrayValue.newArrayValue() .values([ buildObjectLiteral([ - a: StringValue.of("abc") + a: StringValue.of("abc") ]), VariableReference.of("var") ]).build() @@ -818,11 +818,11 @@ class ValuesResolverTest extends Specification { | ArrayValue.newArrayValue() .values([ buildObjectLiteral([ - a: StringValue.of("abc") + a: StringValue.of("abc") ]), buildObjectLiteral([ - a: StringValue.of("xyz"), - b: IntValue.of(789) + a: StringValue.of("xyz"), + b: IntValue.of(789) ]), ]).build() | CoercedVariables.emptyVariables() @@ -831,7 +831,7 @@ class ValuesResolverTest extends Specification { | ArrayValue.newArrayValue() .values([ buildObjectLiteral([ - a: StringValue.of("abc") + a: StringValue.of("abc") ]), VariableReference.of("var") ]).build() From e36b5b774ec86142044578b9dcaaec053c2ce19a Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:40:14 +1100 Subject: [PATCH 024/393] Add valid list test --- .../execution/ValuesResolverTest.groovy | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 8f46042922..8e40ac4433 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -887,6 +887,54 @@ class ValuesResolverTest extends Specification { } + def "getArgumentValues: valid oneOf list input - #testCase"() { + given: "schema defining input object" + def inputObjectType = newInputObject() + .name("oneOfInputObject") + .withAppliedDirective(Directives.OneOfDirective.toAppliedDirective()) + .field(newInputObjectField() + .name("a") + .type(GraphQLString) + .build()) + .field(newInputObjectField() + .name("b") + .type(GraphQLInt) + .build()) + .build() + + when: + def argument = new Argument("arg", inputArray) + def fieldArgumentList = newArgument().name("arg").type(list(inputObjectType)).build() + def values = ValuesResolver.getArgumentValues([fieldArgumentList], [argument], variables, graphQLContext, locale) + + then: + values == expectedValues + + where: + + testCase | inputArray | variables | expectedValues + + '[{ a: "abc"}]' + | ArrayValue.newArrayValue() + .value(buildObjectLiteral([ + a: StringValue.of("abc"), + ])).build() + | CoercedVariables.emptyVariables() + | [arg: [[a: "abc"]]] + + '[{ a: "abc" }, $var ] [{ a: "abc" }, { b: 789 }]' + | ArrayValue.newArrayValue() + .values([ + buildObjectLiteral([ + a: StringValue.of("abc") + ]), + VariableReference.of("var") + ]).build() + | CoercedVariables.of("var": [b: 789]) + | [arg: [[a: "abc"], [b: 789]]] + + } + def "getArgumentValues: invalid oneOf input no values where passed - #testCase"() { given: "schema defining input object" def inputObjectType = newInputObject() From 1e4441e17b4cc0a83eaff2e5320f8b265fa4cea6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:26:42 +0000 Subject: [PATCH 025/393] Bump com.graphql-java:java-dataloader from 3.2.1 to 3.2.2 Bumps [com.graphql-java:java-dataloader](https://github.com/graphql-java/java-dataloader) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/graphql-java/java-dataloader/releases) - [Commits](https://github.com/graphql-java/java-dataloader/compare/v3.2.1...v3.2.2) --- updated-dependencies: - dependency-name: com.graphql-java:java-dataloader dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0f88a5f80a..7e1661f7bb 100644 --- a/build.gradle +++ b/build.gradle @@ -102,7 +102,7 @@ dependencies { compileOnly 'org.jetbrains:annotations:24.0.1' implementation 'org.antlr:antlr4-runtime:' + antlrVersion implementation 'org.slf4j:slf4j-api:' + slf4jVersion - api 'com.graphql-java:java-dataloader:3.2.1' + api 'com.graphql-java:java-dataloader:3.2.2' api 'org.reactivestreams:reactive-streams:' + reactiveStreamsVersion antlr 'org.antlr:antlr4:' + antlrVersion implementation 'com.google.guava:guava:' + guavaVersion From a5051d6850c54b97589af40f13c3bb2d05dcba59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:26:44 +0000 Subject: [PATCH 026/393] Bump com.fasterxml.jackson.core:jackson-databind from 2.15.3 to 2.16.0 Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.15.3 to 2.16.0. - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0f88a5f80a..b81821b8c1 100644 --- a/build.gradle +++ b/build.gradle @@ -112,7 +112,7 @@ dependencies { testImplementation 'org.codehaus.groovy:groovy-json:3.0.19' testImplementation 'com.google.code.gson:gson:2.10.1' testImplementation 'org.eclipse.jetty:jetty-server:11.0.15' - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3' + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0' testImplementation 'org.slf4j:slf4j-simple:' + slf4jVersion testImplementation 'org.awaitility:awaitility-groovy:4.2.0' testImplementation 'com.github.javafaker:javafaker:1.0.2' From d3becaf67fda30c84975d1202ec948932340fde9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:26:47 +0000 Subject: [PATCH 027/393] Bump org.jetbrains:annotations from 24.0.1 to 24.1.0 Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.0.1 to 24.1.0. - [Release notes](https://github.com/JetBrains/java-annotations/releases) - [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md) - [Commits](https://github.com/JetBrains/java-annotations/compare/24.0.1...24.1.0) --- updated-dependencies: - dependency-name: org.jetbrains:annotations dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0f88a5f80a..1f611a43d5 100644 --- a/build.gradle +++ b/build.gradle @@ -99,7 +99,7 @@ jar { } dependencies { - compileOnly 'org.jetbrains:annotations:24.0.1' + compileOnly 'org.jetbrains:annotations:24.1.0' implementation 'org.antlr:antlr4-runtime:' + antlrVersion implementation 'org.slf4j:slf4j-api:' + slf4jVersion api 'com.graphql-java:java-dataloader:3.2.1' From 8f36c681e6234004f2f7b58cbebf77978e01117f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:33:34 +0000 Subject: [PATCH 028/393] Bump google-github-actions/auth from 1.1.1 to 1.2.0 Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 1.1.1 to 1.2.0. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v1.1.1...v1.2.0) --- updated-dependencies: - dependency-name: google-github-actions/auth dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/invoke_test_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index 6d267c4f5a..417cf007eb 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -50,7 +50,7 @@ jobs: - id: 'auth' name: 'Authenticate to Google Cloud' - uses: google-github-actions/auth@v1.1.1 + uses: google-github-actions/auth@v1.2.0 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} From a9cdcc5991235bcd2f4b9fbd9421a5ccb9a8c1bf Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 28 Nov 2023 14:35:38 +1000 Subject: [PATCH 029/393] Update src/main/resources/i18n/Execution_de.properties --- src/main/resources/i18n/Execution_de.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/i18n/Execution_de.properties b/src/main/resources/i18n/Execution_de.properties index e594eb287a..dd194540c7 100644 --- a/src/main/resources/i18n/Execution_de.properties +++ b/src/main/resources/i18n/Execution_de.properties @@ -4,5 +4,5 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # -Execution.handleOneOfNotOneFieldError=Es muss genau ein Schlüssel angegeben werden für OneOf Typ ''{0}''. +Execution.handleOneOfNotOneFieldError=Es muss genau ein Key angegeben werden für OneOf Typ ''{0}''. Execution.handleOneOfValueIsNullError=OneOf type field ''{0}'' darf nicht null sein. From e80a3e71badc12aecc916ddd0ce945ecf92494ee Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Dec 2023 17:30:46 +1100 Subject: [PATCH 030/393] Not quite finished - but has directive filtering --- .../graphql/schema/idl/SchemaPrinter.java | 16 ++++--- .../schema/idl/SchemaPrinterTest.groovy | 46 ++++++++++++++++--- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index 84c5c7e6e2..891eec5014 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -876,7 +876,7 @@ String directivesString(Class parentType, boolea private String directivesString(Class parentType, List directives) { directives = directives.stream() // @deprecated is special - we always print it if something is deprecated - .filter(directive -> options.getIncludeDirective().test(directive.getName()) || isDeprecatedDirective(directive)) + .filter(directive -> options.getIncludeDirective().test(directive.getName())) .filter(options.getIncludeSchemaElement()) .collect(toList()); @@ -909,10 +909,7 @@ private String directiveString(GraphQLAppliedDirective directive) { return ""; } if (!options.getIncludeDirective().test(directive.getName())) { - // @deprecated is special - we always print it if something is deprecated - if (!isDeprecatedDirective(directive)) { - return ""; - } + return ""; } StringBuilder sb = new StringBuilder(); @@ -948,6 +945,13 @@ private String directiveString(GraphQLAppliedDirective directive) { return sb.toString(); } + private boolean isDeprecatedDirectiveAllowed() { + // we ask if the special deprecated directive, + // which can be programmatically on a type without an applied directive, + // should be printed or not + return options.getIncludeDirective().test(DeprecatedDirective.getName()); + } + private boolean isDeprecatedDirective(GraphQLAppliedDirective directive) { return directive.getName().equals(DeprecatedDirective.getName()); } @@ -960,7 +964,7 @@ private boolean hasDeprecatedDirective(List directives) private List addDeprecatedDirectiveIfNeeded(GraphQLDirectiveContainer directiveContainer) { List directives = DirectivesUtil.toAppliedDirectives(directiveContainer); - if (!hasDeprecatedDirective(directives)) { + if (!hasDeprecatedDirective(directives) && isDeprecatedDirectiveAllowed()) { directives = new ArrayList<>(directives); String reason = getDeprecationReason(directiveContainer); GraphQLAppliedDirectiveArgument arg = GraphQLAppliedDirectiveArgument.newArgument() diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy index c0c83ddca8..13e0319233 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy @@ -1556,7 +1556,7 @@ extend type Query { ''' } - def "@deprecated directives are always printed"() { + def "@deprecated directives are NOT always printed - they used to be"() { given: def idl = """ @@ -1588,7 +1588,7 @@ extend type Query { then: result == '''type Field { - deprecated: Enum @deprecated(reason : "No longer supported") + deprecated: Enum } type Query { @@ -1596,11 +1596,11 @@ type Query { } enum Enum { - enumVal @deprecated(reason : "No longer supported") + enumVal } input Input { - deprecated: String @deprecated(reason : "custom reason") + deprecated: String } ''' } @@ -1641,7 +1641,7 @@ type Query { ''' } - def "@deprecated directive are always printed regardless of options"() { + def "@deprecated directive are NOT always printed regardless of options"() { given: def idl = ''' @@ -1660,6 +1660,37 @@ type Query { then: result == '''type Query { + fieldX: String +} +''' + } + + def "@deprecated directive are printed respecting options"() { + given: + def idl = ''' + + type Query { + fieldX : String @deprecated + } + + ''' + def registry = new SchemaParser().parse(idl) + def runtimeWiring = newRuntimeWiring().build() + def options = SchemaGenerator.Options.defaultOptions() + def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) + + when: + def printOptions = defaultOptions().includeDirectives({ dName -> (dName == "deprecated") }) + def result = new SchemaPrinter(printOptions).print(schema) + + then: + result == '''"Marks the field, argument, input field or enum value as deprecated" +directive @deprecated( + "The reason for the deprecation" + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + +type Query { fieldX: String @deprecated(reason : "No longer supported") } ''' @@ -2678,7 +2709,10 @@ input Gun { .query(queryType) .build() when: - def result = "\n" + new SchemaPrinter(noDirectivesOption).print(schema) + + def printOptions = defaultOptions().includeDirectives({d -> true}) + + def result = "\n" + new SchemaPrinter(printOptions).print(schema) println(result) then: From 35cf564f4fdddb2f662c3969f95e50032e5970f6 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 4 Dec 2023 15:20:18 +1100 Subject: [PATCH 031/393] 3385 - fix for the turkish eye --- .../graphql/schema/PropertyFetchingImpl.java | 5 ++- src/main/java/graphql/util/StringKit.java | 20 ++++++++++++ .../schema/PropertyDataFetcherTest.groovy | 32 +++++++++++++++++++ .../groovy/graphql/util/StringKitTest.groovy | 22 +++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/main/java/graphql/util/StringKit.java create mode 100644 src/test/groovy/graphql/util/StringKitTest.groovy diff --git a/src/main/java/graphql/schema/PropertyFetchingImpl.java b/src/main/java/graphql/schema/PropertyFetchingImpl.java index da8b153e3d..9d9eb5a2c6 100644 --- a/src/main/java/graphql/schema/PropertyFetchingImpl.java +++ b/src/main/java/graphql/schema/PropertyFetchingImpl.java @@ -3,6 +3,8 @@ import graphql.GraphQLException; import graphql.Internal; import graphql.schema.fetching.LambdaFetchingSupport; +import graphql.util.EscapeUtil; +import graphql.util.StringKit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +14,7 @@ import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Comparator; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -213,7 +216,7 @@ private Object getPropertyViaGetterMethod(Object object, String propertyName, Gr } private Object getPropertyViaGetterUsingPrefix(Object object, String propertyName, String prefix, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { - String getterName = prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + String getterName = prefix + StringKit.capitalize(propertyName); Method method = methodFinder.apply(object.getClass(), getterName); return invokeMethod(object, singleArgumentValue, method, takesSingleArgumentTypeAsOnlyArgument(method)); } diff --git a/src/main/java/graphql/util/StringKit.java b/src/main/java/graphql/util/StringKit.java new file mode 100644 index 0000000000..20fcea1b1f --- /dev/null +++ b/src/main/java/graphql/util/StringKit.java @@ -0,0 +1,20 @@ +package graphql.util; + +import java.util.Locale; + +public class StringKit { + + public static String capitalize(String s) { + if (s != null && !s.isEmpty()) { + StringBuilder sb = new StringBuilder(); + // see https://github.com/graphql-java/graphql-java/issues/3385 + sb.append(s.substring(0, 1).toUpperCase(Locale.ROOT)); + if (s.length() > 1) { + sb.append(s.substring(1)); + } + return sb.toString(); + } + return s; + } + +} diff --git a/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy b/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy index f771835575..b9fae5e3f0 100644 --- a/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy +++ b/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy @@ -637,6 +637,38 @@ class PropertyDataFetcherTest extends Specification { result == "bar" } + class BaseObject { + private String id + + String getId() { + return id + } + + void setId(String value) { + id = value; + } + } + + class OtherObject extends BaseObject {} + + def "Can access private property from base class that starts with i in Turkish"() { + // see https://github.com/graphql-java/graphql-java/issues/3385 + given: + Locale oldLocale = Locale.getDefault() + Locale.setDefault(new Locale("tr", "TR")) + + def environment = env(new OtherObject(id: "aValue")) + def fetcher = PropertyDataFetcher.fetching("id") + + when: + String propValue = fetcher.get(environment) + + then: + propValue == 'aValue' + + cleanup: + Locale.setDefault(oldLocale) + } /** * Classes from issue to ensure we reproduce as reported by customers * diff --git a/src/test/groovy/graphql/util/StringKitTest.groovy b/src/test/groovy/graphql/util/StringKitTest.groovy new file mode 100644 index 0000000000..a2a428bce6 --- /dev/null +++ b/src/test/groovy/graphql/util/StringKitTest.groovy @@ -0,0 +1,22 @@ +package graphql.util + +import spock.lang.Specification + +class StringKitTest extends Specification { + + + def "can capitalise"() { + expect: + + def actual = StringKit.capitalize(input) + actual == expected + + where: + input | expected + null | null + "" | "" + "a" | "A" + "abc" | "Abc" + + } +} From 5a9ee928de8f306de8ace75f68ea89ca5c30addb Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 4 Dec 2023 15:41:23 +1100 Subject: [PATCH 032/393] 3385 - fix for the turkish eye - extra capitalize place --- src/main/java/graphql/schema/diff/SchemaDiff.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/graphql/schema/diff/SchemaDiff.java b/src/main/java/graphql/schema/diff/SchemaDiff.java index 9012c217f9..c2ae641b0c 100644 --- a/src/main/java/graphql/schema/diff/SchemaDiff.java +++ b/src/main/java/graphql/schema/diff/SchemaDiff.java @@ -39,6 +39,7 @@ import static graphql.language.TypeKind.getTypeKind; import static graphql.schema.idl.TypeInfo.getAstDesc; import static graphql.schema.idl.TypeInfo.typeInfo; +import static graphql.util.StringKit.capitalize; /** * The SchemaDiff is called with a {@link DiffSet} and will report the @@ -974,15 +975,6 @@ private Map sortedMap(List listOfNamedThings, Function(map); } - private static String capitalize(String name) { - if (name != null && name.length() != 0) { - char[] chars = name.toCharArray(); - chars[0] = Character.toUpperCase(chars[0]); - return new String(chars); - } else { - return name; - } - } private String mkDotName(String... objectNames) { return String.join(".", objectNames); From d72ed3726b073db0a2ae3cb69fc8002883eff81c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:00:29 +0000 Subject: [PATCH 033/393] Bump google-github-actions/auth from 1.2.0 to 2.0.0 Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 1.2.0 to 2.0.0. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v1.2.0...v2.0.0) --- updated-dependencies: - dependency-name: google-github-actions/auth dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/invoke_test_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index 417cf007eb..1b449a6a4f 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -50,7 +50,7 @@ jobs: - id: 'auth' name: 'Authenticate to Google Cloud' - uses: google-github-actions/auth@v1.2.0 + uses: google-github-actions/auth@v2.0.0 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} From c89fbe815beb03d3e891a7f0a88db03f68a66cfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:00:33 +0000 Subject: [PATCH 034/393] Bump actions/setup-java from 3 to 4 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/master.yml | 2 +- .github/workflows/pull_request.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 975b85c197..a7d55e90b0 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v1 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'corretto' diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b361a48d8f..ce85c881db 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v1 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'corretto' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d1b866047..773bcd66ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v1 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'corretto' From 98566789b95f70758ccfc79f5b9840665d288c8c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 5 Dec 2023 16:09:08 +1100 Subject: [PATCH 035/393] WIP: Implement support for defer in ExecutableNormalizedOperationFactory --- src/main/java/graphql/Directives.java | 57 +++ .../normalized/ExecutableNormalizedField.java | 19 + .../ExecutableNormalizedOperationFactory.java | 72 +++- ...tableNormalizedOperationToAstCompiler.java | 2 +- .../incremental/DeferExecution.java | 40 ++ .../normalized/incremental/DeferLabel.java | 34 ++ .../incremental/IncrementalNodes.java | 51 +++ ...NormalizedOperationFactoryDeferTest.groovy | 399 ++++++++++++++++++ ...izedOperationToAstCompilerDeferTest.groovy | 114 +++++ 9 files changed, 766 insertions(+), 22 deletions(-) create mode 100644 src/main/java/graphql/normalized/incremental/DeferExecution.java create mode 100644 src/main/java/graphql/normalized/incremental/DeferLabel.java create mode 100644 src/main/java/graphql/normalized/incremental/IncrementalNodes.java create mode 100644 src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy create mode 100644 src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 50e570e14a..61358c5b68 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -1,6 +1,7 @@ package graphql; +import graphql.language.BooleanValue; import graphql.language.Description; import graphql.language.DirectiveDefinition; import graphql.language.StringValue; @@ -33,6 +34,7 @@ public class Directives { private static final String SPECIFIED_BY = "specifiedBy"; private static final String DEPRECATED = "deprecated"; private static final String ONE_OF = "oneOf"; + private static final String DEFER = "defer"; public static final String NO_LONGER_SUPPORTED = "No longer supported"; public static final DirectiveDefinition DEPRECATED_DIRECTIVE_DEFINITION; @@ -40,6 +42,13 @@ public class Directives { @ExperimentalApi public static final DirectiveDefinition ONE_OF_DIRECTIVE_DEFINITION; + /** + * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in + * directive that is not available unless it is explicitly put into the schema. + */ +// @ExperimentalApi +// public static final DirectiveDefinition DEFER_DIRECTIVE_DEFINITION; + static { DEPRECATED_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() @@ -75,8 +84,56 @@ public class Directives { .directiveLocation(newDirectiveLocation().name(INPUT_OBJECT.name()).build()) .description(createDescription("Indicates an Input Object is a OneOf Input Object.")) .build(); + +// DEFER_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() +// .name(DEFER) +// .description(createDescription("This directive allows results to be deferred during execution")) +// .directiveLocation(newDirectiveLocation().name(FRAGMENT_SPREAD.name()).build()) +// .directiveLocation(newDirectiveLocation().name(INLINE_FRAGMENT.name()).build()) +// .inputValueDefinition( +// newInputValueDefinition() +// .name("if") +// .description(createDescription("Deferred behaviour is controlled by this argument")) +// .type(newTypeName().name("Boolean").build()) +// .defaultValue(BooleanValue.newBooleanValue(true).build()) +// .build()) +// .inputValueDefinition( +// newInputValueDefinition() +// // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: +// // > `label` must not be provided as a variable. +// // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query +// .name("label") +// .description(createDescription("A unique label that represents the fragment being deferred")) +// .type(newTypeName().name("Boolean").build()) +// .defaultValue(BooleanValue.newBooleanValue(true).build()) +// .build()) +// .build(); } + /** + * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in + * directive that is not available unless it is explicitly put into the schema. + */ + public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() + .name("defer") + .description("This directive allows results to be deferred during execution") + .validLocations(FRAGMENT_SPREAD, INLINE_FRAGMENT) + .argument(newArgument() + .name("if") + .type(nonNull(GraphQLBoolean)) + .description("Deferred behaviour is controlled by this argument") + .defaultValueLiteral(BooleanValue.newBooleanValue(true).build()) + ) + .argument(newArgument() + // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: + // > `label` must not be provided as a variable. + // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query + .name("label") + .type(GraphQLString) + .description("A unique label that represents the fragment being deferred") + ) + .build(); + public static final GraphQLDirective IncludeDirective = GraphQLDirective.newDirective() .name("include") .description("Directs the executor to include this field or fragment only when the `if` argument is true") diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 41ddd594b3..73c1df4d0b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -9,6 +9,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -63,6 +64,7 @@ public class ExecutableNormalizedField { private final String fieldName; private final int level; + private final DeferExecution deferExecution; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -74,6 +76,7 @@ private ExecutableNormalizedField(Builder builder) { this.children = builder.children; this.level = builder.level; this.parent = builder.parent; + this.deferExecution = builder.deferExecution; } /** @@ -365,6 +368,14 @@ public String getSingleObjectTypeName() { } + /** + * TODO Javadoc + * @return + */ + public DeferExecution getDeferExecution() { + return deferExecution; + } + /** * @return a helper method show field details */ @@ -588,6 +599,8 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); + private DeferExecution deferExecution; + private Builder() { } @@ -596,6 +609,7 @@ private Builder(ExecutableNormalizedField existing) { this.normalizedArguments = existing.normalizedArguments; this.astArguments = existing.astArguments; this.resolvedArguments = existing.resolvedArguments; + this.deferExecution = existing.deferExecution; this.objectTypeNames = new LinkedHashSet<>(existing.getObjectTypeNames()); this.fieldName = existing.getFieldName(); this.children = new ArrayList<>(existing.children); @@ -656,6 +670,11 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } + public Builder deferExecution(DeferExecution deferExecution) { + this.deferExecution = deferExecution; + return this; + } + public ExecutableNormalizedField build() { return new ExecutableNormalizedField(this); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 1124c9b7ce..1af327ae39 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -26,6 +26,9 @@ import graphql.language.Selection; import graphql.language.SelectionSet; import graphql.language.VariableDefinition; +import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.DeferLabel; +import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -146,6 +149,7 @@ public int getMaxChildrenDepth() { } private final ConditionalNodes conditionalNodes = new ConditionalNodes(); + private final IncrementalNodes incrementalNodes = new IncrementalNodes(); /** * This will create a runtime representation of the graphql operation that would be executed @@ -155,7 +159,6 @@ public int getMaxChildrenDepth() { * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param coercedVariableValues the coerced variables to use - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( @@ -181,7 +184,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param operationDefinition the operation to be executed * @param fragments a set of fragments associated with the operation * @param coercedVariableValues the coerced variables to use - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation(GraphQLSchema graphQLSchema, @@ -204,7 +206,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param rawVariables the raw variables to be coerced - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, @@ -229,7 +230,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param rawVariables the raw variables that have not yet been coerced * @param locale the {@link Locale} to use during coercion * @param graphQLContext the {@link GraphQLContext} to use during coercion - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables( @@ -258,7 +258,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param operationName the operation name to use * @param rawVariables the raw variables that have not yet been coerced * @param options the {@link Options} to use for parsing - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, @@ -465,7 +464,8 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam fieldAndAstParent.field.getSelectionSet(), collectedFields, (GraphQLCompositeType) astParentType, - possibleObjects + possibleObjects, + null ); } Map> fieldsByName = fieldsByResultKey(collectedFields); @@ -492,7 +492,7 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Set possibleObjects = ImmutableSet.of(rootType); List collectedFields = new ArrayList<>(); - collectFromSelectionSet(parameters, operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects); + collectFromSelectionSet(parameters, operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects, null); // group by result key Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); @@ -554,33 +554,50 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p .fieldName(fieldName) .level(level) .parent(parent) + .deferExecution(collectedFieldGroup.deferExecution) .build(); } private static class CollectedFieldGroup { Set objectTypes; Set fields; + DeferExecution deferExecution; - public CollectedFieldGroup(Set fields, Set objectTypes) { + public CollectedFieldGroup(Set fields, Set objectTypes, DeferExecution deferExecution) { this.fields = fields; this.objectTypes = objectTypes; + this.deferExecution = deferExecution; } } private List groupByCommonParents(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); + DeferExecution deferExecution = null; for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); + + DeferLabel collectedDeferLabel = collectedField.deferLabel; + + if (collectedDeferLabel == null) { + continue; + } + + if (deferExecution == null) { + deferExecution = new DeferExecution(); + } + + deferExecution.addLabel(collectedDeferLabel); } + Set allRelevantObjects = objectTypes.build(); Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects)); + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecution)); } ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType))); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecution)); } return result.build(); } @@ -590,15 +607,16 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet SelectionSet selectionSet, List result, GraphQLCompositeType astTypeCondition, - Set possibleObjects + Set possibleObjects, + DeferLabel deferLabel ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { - collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition); + collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition, deferLabel); } else if (selection instanceof InlineFragment) { - collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition); + collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition, deferLabel); } else if (selection instanceof FragmentSpread) { - collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects); + collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects, deferLabel); } } } @@ -608,10 +626,13 @@ private static class CollectedField { Set objectTypes; GraphQLCompositeType astTypeCondition; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition) { + DeferLabel deferLabel; + + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferLabel deferLabel) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; + this.deferLabel = deferLabel; } public boolean isAbstract() { @@ -626,7 +647,8 @@ public boolean isConcrete() { private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameters, List result, FragmentSpread fragmentSpread, - Set possibleObjects + Set possibleObjects, + DeferLabel deferLabel ) { if (!conditionalNodes.shouldInclude(fragmentSpread, parameters.getCoercedVariableValues(), @@ -642,9 +664,12 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter parameters.getGraphQLContext())) { return; } + + DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); + GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); + collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); } @@ -652,7 +677,8 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter List result, InlineFragment inlineFragment, Set possibleObjects, - GraphQLCompositeType astTypeCondition + GraphQLCompositeType astTypeCondition, + DeferLabel deferLabel ) { if (!conditionalNodes.shouldInclude(inlineFragment, parameters.getCoercedVariableValues(), parameters.getGraphQLSchema(), parameters.getGraphQLContext())) { return; @@ -665,14 +691,18 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); } - collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); + + DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); + + collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); } private void collectField(FieldCollectorNormalizedQueryParams parameters, List result, Field field, Set possibleObjectTypes, - GraphQLCompositeType astTypeCondition + GraphQLCompositeType astTypeCondition, + DeferLabel deferLabel ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), @@ -684,7 +714,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, if (possibleObjectTypes.isEmpty()) { return; } - result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition)); + result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferLabel)); } private Set narrowDownPossibleObjects(Set currentOnes, diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 7dc3d11f32..9d323f1914 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -46,7 +46,7 @@ /** * This class can take a list of {@link ExecutableNormalizedField}s and compiling out a * normalised operation {@link Document} that would represent how those fields - * maybe executed. + * may be executed. *

* This is essentially the reverse of {@link ExecutableNormalizedOperationFactory} which takes * operation text and makes {@link ExecutableNormalizedField}s from it, this takes {@link ExecutableNormalizedField}s diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java new file mode 100644 index 0000000000..892a253238 --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -0,0 +1,40 @@ +package graphql.normalized.incremental; + +import graphql.ExperimentalApi; + +import java.util.HashSet; +import java.util.Set; + +/** + * This class holds information about the defer execution of an ENF. + *

+ * Given that an ENF can be linked with numerous defer labels, a {@link DeferExecution} instance comprises a + * collection of these labels. + *

+ * For example, this query: + *

+ *   query q {
+ *    dog {
+ *      ... @defer(label: "name-defer") {
+ *        name
+ *      }
+ *      ... @defer(label: "another-name-defer") {
+ *        name
+ *      }
+ *    }
+ *  }
+ *  
+ * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" + */ +@ExperimentalApi +public class DeferExecution { + private final Set labels = new HashSet<>(); + + public void addLabel(DeferLabel label) { + this.labels.add(label); + } + + public Set getLabels() { + return this.labels; + } +} diff --git a/src/main/java/graphql/normalized/incremental/DeferLabel.java b/src/main/java/graphql/normalized/incremental/DeferLabel.java new file mode 100644 index 0000000000..d965e5636d --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/DeferLabel.java @@ -0,0 +1,34 @@ +package graphql.normalized.incremental; + +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Holds the value of the 'label' argument of a defer directive. + */ +public class DeferLabel { + private final String value; + + public DeferLabel(@Nullable String value) { + this.value = value; + } + + @Nullable + public String getValue() { + return value; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.value); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof DeferLabel) { + return Objects.equals(this.value, ((DeferLabel) obj).value); + } + + return false; + } +} diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java new file mode 100644 index 0000000000..4763c6c9d2 --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -0,0 +1,51 @@ +package graphql.normalized.incremental; + +import graphql.Assert; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.CoercedVariables; +import graphql.execution.ValuesResolver; +import graphql.language.Directive; +import graphql.language.NodeUtil; +import graphql.schema.GraphQLSchema; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static graphql.Directives.DeferDirective; + +@Internal +public class IncrementalNodes { + + public DeferLabel getDeferLabel( + Map variables, + List directives + ) { + Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); + + if (deferDirective != null) { + Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); + + Object flag = argumentValues.get("if"); + Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + if(!((Boolean) flag)) { + return null; + } + + Object label = argumentValues.get("label"); + + if(label == null) { + return new DeferLabel(null); + } + + Assert.assertTrue(label instanceof String, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + return new DeferLabel((String) label); + + } + + return null; + } +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy new file mode 100644 index 0000000000..110a3353f0 --- /dev/null +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -0,0 +1,399 @@ +package graphql.normalized + +import graphql.AssertException +import graphql.ExecutionInput +import graphql.GraphQL +import graphql.TestUtil +import graphql.execution.RawVariables +import graphql.language.Document +import graphql.schema.GraphQLSchema +import graphql.util.TraversalControl +import graphql.util.Traverser +import graphql.util.TraverserContext +import graphql.util.TraverserVisitorStub +import spock.lang.Specification + +class ExecutableNormalizedOperationFactoryDeferTest extends Specification { + String schema = """ + directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD + + type Query { + dog: Dog + } + + type Dog { + name: String + breed: String + owner: Person + friends: [Dog] + } + + type Person { + firstname: String + lastname: String + friends: [Person] + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + def "defer on a single field via inline fragment without type"() { + given: + + String query = ''' + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name', + 'Dog.breed defer[breed-defer]', + ] + } + + def "defer on a single field via inline fragment with type"() { + given: + + String query = ''' + query q { + dog { + name + ... on Dog @defer(label: "breed-defer") { + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name', + 'Dog.breed defer[breed-defer]', + ] + } + + def "defer on 2 fields"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "breed-defer") { + name + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[breed-defer]', + 'Dog.breed defer[breed-defer]', + ] + } + + def "defer on a fragment definition"() { + given: + + String query = ''' + query q { + dog { + ... DogFrag @defer(label: "breed-defer") + } + } + + fragment DogFrag on Dog { + name + breed + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[breed-defer]', + 'Dog.breed defer[breed-defer]', + ] + } + + def "multiple defer on same field with different labels"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "name-defer") { + name + } + + ... @defer(label: "another-name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer,another-name-defer]', + ] + } + + def "multiple fields and a single defer"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "name-defer") { + name + } + + ... { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer]', + ] + } + + def "multiple fields and a single defer - no label"() { + given: + + String query = ''' + query q { + dog { + ... @defer { + name + } + + ... { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[null]', + ] + } + + def "multiple fields and a multiple defers with same label"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label:"name-defer") { + name + } + + ... @defer(label:"name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer]', + ] + } + + def "nested defers - no label"() { + given: + + String query = ''' + query q { + dog { + ... @defer { + name + owner { + firstname + ... @defer { + lastname + } + } + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[null]', + 'Dog.owner defer[null]', + 'Person.firstname', + 'Person.lastname defer[null]', + ] + } + + def "nested defers - with labels"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label:"dog-defer") { + name + owner { + firstname + ... @defer(label: "lastname-defer") { + lastname + } + } + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[dog-defer]', + 'Dog.owner defer[dog-defer]', + 'Person.firstname', + 'Person.lastname defer[lastname-defer]', + ] + } + + def "nesting defer blocks that would always result in no data are ignored"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "one") { + ... @defer(label: "two") { + ... @defer(label: "three") { + name + } + } + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[three]', + ] + } + + private List executeQueryAndPrintTree(String query, Map variables) { + assertValidQuery(graphQLSchema, query, variables) + Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + return printTreeWithIncrementalExecutionDetails(tree) + } + + private List printTreeWithIncrementalExecutionDetails(ExecutableNormalizedOperation queryExecutionTree) { + def result = [] + Traverser traverser = Traverser.depthFirst({ it.getChildren() }) + traverser.traverse(queryExecutionTree.getTopLevelFields(), new TraverserVisitorStub() { + @Override + TraversalControl enter(TraverserContext context) { + ExecutableNormalizedField queryExecutionField = context.thisNode() + result << queryExecutionField.printDetails() + printDeferExecutionDetails(queryExecutionField) + return TraversalControl.CONTINUE + } + + String printDeferExecutionDetails(ExecutableNormalizedField field) { + if (field.getDeferExecution() == null) { + return "" + } + + def deferLabels = field.getDeferExecution().labels + .collect {it.value} + .join(",") + + return " defer[" + deferLabels + "]" + } + }) + + result + } + + private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() + def ei = ExecutionInput.newExecutionInput(query).variables(variables).build() + assert graphQL.execute(ei).errors.size() == 0 + } +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy new file mode 100644 index 0000000000..2400fa8ca0 --- /dev/null +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -0,0 +1,114 @@ +package graphql.normalized + +import graphql.GraphQL +import graphql.TestUtil +import graphql.execution.RawVariables +import graphql.language.AstPrinter +import graphql.language.AstSorter +import graphql.language.Document +import graphql.schema.GraphQLSchema +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.TestLiveMockedWiringFactory +import graphql.schema.scalars.JsonScalar +import spock.lang.Specification + +import static graphql.ExecutionInput.newExecutionInput +import static graphql.language.OperationDefinition.Operation.QUERY +import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocument + +class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification { + VariablePredicate noVariables = new VariablePredicate() { + @Override + boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { + return false + } + } + + VariablePredicate jsonVariables = new VariablePredicate() { + @Override + boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { + "JSON" == normalizedInputValue.unwrappedTypeName && normalizedInputValue.value != null + } + } + + VariablePredicate allVariables = new VariablePredicate() { + @Override + boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { + return true + } + } + + String sdl = """ + directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD + + type Query { + dog: Dog + } + + type Dog { + name: String + breed: String + owner: Person + friends: [Dog] + } + + type Person { + firstname: String + lastname: String + friends: [Person] + } + """ + + def "simple defer"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } +''' + } + + private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { + assertValidQuery(schema, query, variables) + Document originalDocument = TestUtil.parseQuery(query) + + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) + } + + private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { + return createNormalizedTree(schema, query, variables).getTopLevelFields() + } + + private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() + assert graphQL.execute(newExecutionInput().query(query).variables(variables)).errors.isEmpty() + } + + GraphQLSchema mkSchema(String sdl) { + def wiringFactory = new TestLiveMockedWiringFactory([JsonScalar.JSON_SCALAR]) + def runtimeWiring = RuntimeWiring.newRuntimeWiring() + .wiringFactory(wiringFactory).build() + TestUtil.schema(sdl, runtimeWiring) + } +} From 5c25875980a4c32d0944a9b5e83fc8f213d764c8 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:11:09 +1100 Subject: [PATCH 036/393] Add tag to exempt PRs and issues from the stale bot --- .github/workflows/stale-pr-issue.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale-pr-issue.yml b/.github/workflows/stale-pr-issue.yml index af1359b548..1aee149f1d 100644 --- a/.github/workflows/stale-pr-issue.yml +++ b/.github/workflows/stale-pr-issue.yml @@ -32,6 +32,7 @@ jobs: close-issue-message: > Hello, as this issue has been inactive for 90 days, we're closing the issue. If you would like to resume the discussion, please create a new issue. + exempt-issue-labels: keep-open # PULL REQUESTS ----------------------------------------------------- # PRs will be closed after 90 days of inactive (60 to mark as stale + 30 to close) @@ -41,4 +42,5 @@ jobs: If you would like to continue working on this pull request, please make an update within the next 30 days, or we'll close the pull request. close-pr-message: > Hello, as this pull request has been inactive for 90 days, we're closing this pull request. - We always welcome contributions, and if you would like to continue, please open a new pull request. \ No newline at end of file + We always welcome contributions, and if you would like to continue, please open a new pull request. + exempt-pr-labels: keep-open From 37b1c4a671dd79b2b9684efcb8aa4c06a13957ba Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 5 Dec 2023 17:55:18 +1100 Subject: [PATCH 037/393] WIP: ENF to AST Compiler basic implementation --- ...tableNormalizedOperationToAstCompiler.java | 110 ++++++++--- .../incremental/DeferExecution.java | 6 +- ...NormalizedOperationFactoryDeferTest.groovy | 43 +++- ...izedOperationToAstCompilerDeferTest.groovy | 185 +++++++++++++++++- 4 files changed, 301 insertions(+), 43 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 9d323f1914..74250e80ae 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -18,8 +18,10 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; +import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; +import graphql.normalized.incremental.DeferLabel; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -32,6 +34,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import static graphql.collect.ImmutableKit.emptyList; @@ -82,7 +85,7 @@ public Map getVariables() { /** * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s - * + *

* The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * @@ -91,7 +94,6 @@ public Map getVariables() { * @param operationName the name of the operation to use * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -99,22 +101,21 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @Nullable String operationName, @NotNull List topLevelFields, @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema,operationKind,operationName,topLevelFields,Map.of(),variablePredicate); + return compileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); } /** * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s - * + *

* The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * - * @param schema the graphql schema to use - * @param operationKind the kind of operation - * @param operationName the name of the operation to use - * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from - * @param normalizedFieldToQueryDirectives the map of normalized field to query directives - * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param normalizedFieldToQueryDirectives the map of normalized field to query directives + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -153,27 +154,61 @@ private static List> subselectionsForNormalizedField(GraphQLSchema // All conditional fields go here instead of directly to selections, so they can be grouped together // in the same inline fragment in the output - Map> fieldsByTypeCondition = new LinkedHashMap<>(); + // + Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) + .forEach((objectTypeName, field) -> { + if (nf.getDeferExecution() == null) { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) + .add(field); + } else { + nf.getDeferExecution().getLabels().stream() + .map(DeferLabel::getValue) + .forEach(label -> fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, new DeferLabel(label)), ignored -> new ArrayList<>()) + .add(field)); + } + }); + + } else if (nf.getDeferExecution() != null) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) .forEach((objectTypeName, field) -> - fieldsByTypeCondition - .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) - .add(field)); + nf.getDeferExecution().getLabels().stream() + .map(DeferLabel::getValue) + .forEach(label -> fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) + .add(field)) + ); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives,variableAccumulator)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator)); } } - fieldsByTypeCondition.forEach((objectTypeName, fields) -> { - TypeName typeName = newTypeName(objectTypeName).build(); - InlineFragment inlineFragment = newInlineFragment() - .typeCondition(typeName) - .selectionSet(selectionSet(fields)) - .build(); - selections.add(inlineFragment); + fieldsByFragmentDetails.forEach((typeAndDeferPair, fields) -> { + InlineFragment.Builder fragmentBuilder = newInlineFragment() + .selectionSet(selectionSet(fields)); + + if (typeAndDeferPair.typeName != null) { + TypeName typeName = newTypeName(typeAndDeferPair.typeName).build(); + fragmentBuilder.typeCondition(typeName); + } + + if (typeAndDeferPair.deferLabel != null) { + Directive.Builder deferBuilder = Directive.newDirective().name("defer"); + + if (typeAndDeferPair.deferLabel.getValue() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferLabel.getValue())).build()); + } + + fragmentBuilder.directive(deferBuilder.build()); + } + + + selections.add(fragmentBuilder.build()); }); return selections.build(); @@ -189,7 +224,7 @@ private static Map selectionForNormalizedField(GraphQLSchema sche Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField,normalizedFieldToQueryDirectives, variableAccumulator)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator)); } return groupedFields; @@ -230,9 +265,9 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, .alias(executableNormalizedField.getAlias()) .selectionSet(selectionSet) .arguments(arguments); - if(queryDirectives == null || queryDirectives.getImmediateAppliedDirectivesByField().isEmpty() ){ + if (queryDirectives == null || queryDirectives.getImmediateAppliedDirectivesByField().isEmpty()) { return builder.build(); - }else { + } else { List directives = queryDirectives.getImmediateAppliedDirectivesByField().keySet().stream().flatMap(field -> field.getDirectives().stream()).collect(Collectors.toList()); return builder .directives(directives) @@ -326,4 +361,27 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, return Assert.assertShouldNeverHappen("Unknown operation kind " + operationKind); } + // TODO: This name is terrible + public static class ExecutionFragmentDetails { + private final String typeName; + private final DeferLabel deferLabel; + + public ExecutionFragmentDetails(String typeName, DeferLabel deferLabel) { + this.typeName = typeName; + this.deferLabel = deferLabel; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExecutionFragmentDetails that = (ExecutionFragmentDetails) o; + return Objects.equals(typeName, that.typeName) && Objects.equals(deferLabel, that.deferLabel); + } + + @Override + public int hashCode() { + return Objects.hash(typeName, deferLabel); + } + } } diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 892a253238..7754d6de83 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -2,7 +2,7 @@ import graphql.ExperimentalApi; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; /** @@ -24,11 +24,11 @@ * } * } * - * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" + * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" */ @ExperimentalApi public class DeferExecution { - private final Set labels = new HashSet<>(); + private final Set labels = new LinkedHashSet<>(); public void addLabel(DeferLabel label) { this.labels.add(label); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 110a3353f0..8c2f844d7a 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -16,23 +16,25 @@ import spock.lang.Specification class ExecutableNormalizedOperationFactoryDeferTest extends Specification { String schema = """ directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD type Query { dog: Dog + animal: Animal + } + + interface Animal { + name: String } - type Dog { + type Dog implements Animal { name: String breed: String owner: Person - friends: [Dog] } type Person { firstname: String lastname: String - friends: [Person] } """ @@ -327,6 +329,39 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "nested defers - with named spreads"() { + given: + + String query = ''' + query q { + animal { + name + ... on Dog @defer(label:"dog-defer") { + owner { + firstname + ... @defer(label: "lastname-defer") { + lastname + } + } + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + 'Dog.name', + 'Dog.owner defer[dog-defer]', + 'Person.firstname', + 'Person.lastname defer[lastname-defer]', + ] + } + def "nesting defer blocks that would always result in no data are ignored"() { given: diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 2400fa8ca0..78d518281c 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -40,23 +40,24 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification String sdl = """ directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD type Query { dog: Dog + animal: Animal + } + + interface Animal { + name: String } - type Dog { + type Dog implements Animal { name: String breed: String owner: Person - friends: [Dog] } type Person { firstname: String - lastname: String - friends: [Person] } """ @@ -78,13 +79,177 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ - dog { - name - ... @defer(label: "breed-defer") { - breed - } + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } +} +''' + } + + def "simple defer with named spread"() { + String query = """ + query q { + dog { + name + ... on Dog @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } +} +''' + } + + def "multiple labels on the same field"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + ... @defer(label: "breed-defer-2") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + ... @defer(label: "breed-defer-2") { + breed + } + } +} +''' + } + + def "multiple defers with same label on the same field"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + ... @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } +} +''' + } + + def "multiple defers without label on the same field"() { + String query = """ + query q { + dog { + name + ... @defer { + breed + } + ... @defer { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer { + breed } } +} +''' + } + + def "defer on type spread"() { + String query = """ + query q { + animal { + ... on Dog @defer { + breed + } + ... on Dog { + name + } + ... on Dog @defer(label: "owner-defer") { + owner { + firstname + } + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + name + ... on Dog @defer { + breed + } + ... on Dog @defer(label: "owner-defer") { + owner { + firstname + } + } + } +} ''' } From d6265ab58226b4843d80ff16338eeb51da5a3ffd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:19:47 +0000 Subject: [PATCH 038/393] Bump actions/stale from 8 to 9 Bumps [actions/stale](https://github.com/actions/stale) from 8 to 9. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v8...v9) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/stale-pr-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-pr-issue.yml b/.github/workflows/stale-pr-issue.yml index 1aee149f1d..f36da60e2a 100644 --- a/.github/workflows/stale-pr-issue.yml +++ b/.github/workflows/stale-pr-issue.yml @@ -15,7 +15,7 @@ jobs: close-pending: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: # GLOBAL ------------------------------------------------------------ # Exempt any PRs or issues already added to a milestone From 8b68720953c8dbfbfa8784f16f9b3e55d63da895 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:08:01 +1100 Subject: [PATCH 039/393] Fix edge case with non-conditional fields in fragments --- ...tableNormalizedOperationToAstCompiler.java | 16 +-- ...izedOperationToAstCompilerDeferTest.groovy | 128 ++++++++++++++++++ 2 files changed, 135 insertions(+), 9 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 74250e80ae..27b97a89f6 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -175,14 +175,12 @@ private static List> subselectionsForNormalizedField(GraphQLSchema }); } else if (nf.getDeferExecution() != null) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) - .forEach((objectTypeName, field) -> - nf.getDeferExecution().getLabels().stream() - .map(DeferLabel::getValue) - .forEach(label -> fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) - .add(field)) - ); + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator); + nf.getDeferExecution().getLabels().stream() + .map(DeferLabel::getValue) + .forEach(label -> fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) + .add(field)); } else { selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator)); } @@ -362,7 +360,7 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, } // TODO: This name is terrible - public static class ExecutionFragmentDetails { + private static class ExecutionFragmentDetails { private final String typeName; private final DeferLabel deferLabel; diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 78d518281c..fc090eb9a4 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -56,8 +56,21 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification owner: Person } + type Cat implements Animal { + name: String + breed: String + color: String + siblings: [Cat] + } + + type Fish implements Animal { + name: String + } + type Person { firstname: String + lastname: String + bestFriend: Person } """ @@ -253,6 +266,121 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "2 fragments on non-conditional fields"() { + String query = """ + query q { + animal { + ... on Cat @defer { + name + } + ... on Animal @defer { + name + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... @defer { + name + } + } +} +''' + } + + def "2 fragments on conditional fields"() { + String query = """ + query q { + animal { + ... on Cat @defer { + breed + } + ... on Dog @defer { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + name + } +} +''' + } + + def "nested defer"() { + String query = """ + query q { + animal { + ... on Cat @defer { + name + } + ... on Animal @defer { + name + ... on Dog @defer { + owner { + firstname + ... @defer { + lastname + } + ... @defer { + bestFriend { + firstname + ... @defer { + lastname + } + } + } + } + } + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... @defer { + name + } + ... on Dog @defer { + owner { + firstname + ... @defer { + bestFriend { + firstname + ... @defer { + lastname + } + } + lastname + } + } + } + } +} +''' + } + private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) From c85cbed61ad1a99f01e499df03a7b7c667f2cacd Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:23:13 +1100 Subject: [PATCH 040/393] Fix comment and test --- ...tableNormalizedOperationToAstCompiler.java | 2 +- ...izedOperationToAstCompilerDeferTest.groovy | 25 +++++++------------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 27b97a89f6..9ae035e975 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -152,7 +152,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); - // All conditional fields go here instead of directly to selections, so they can be grouped together + // All conditional and deferred fields go here instead of directly to selections, so they can be grouped together // in the same inline fragment in the output // Map> fieldsByFragmentDetails = new LinkedHashMap<>(); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index fc090eb9a4..ac313785a0 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -24,20 +24,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } } - VariablePredicate jsonVariables = new VariablePredicate() { - @Override - boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { - "JSON" == normalizedInputValue.unwrappedTypeName && normalizedInputValue.value != null - } - } - - VariablePredicate allVariables = new VariablePredicate() { - @Override - boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { - return true - } - } - String sdl = """ directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT @@ -252,10 +238,12 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { - name ... on Dog @defer { breed } + ... on Dog { + name + } ... on Dog @defer(label: "owner-defer") { owner { firstname @@ -316,7 +304,12 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { - name + ... on Cat @defer { + breed + } + ... on Dog @defer { + breed + } } } ''' From df767cb66861197fea441da64eb29015efc84c59 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:31:28 +1100 Subject: [PATCH 041/393] Remove commented out code --- src/main/java/graphql/Directives.java | 33 +-------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 61358c5b68..98ca5b5625 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -42,14 +42,6 @@ public class Directives { @ExperimentalApi public static final DirectiveDefinition ONE_OF_DIRECTIVE_DEFINITION; - /** - * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in - * directive that is not available unless it is explicitly put into the schema. - */ -// @ExperimentalApi -// public static final DirectiveDefinition DEFER_DIRECTIVE_DEFINITION; - - static { DEPRECATED_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() .name(DEPRECATED) @@ -84,36 +76,13 @@ public class Directives { .directiveLocation(newDirectiveLocation().name(INPUT_OBJECT.name()).build()) .description(createDescription("Indicates an Input Object is a OneOf Input Object.")) .build(); - -// DEFER_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() -// .name(DEFER) -// .description(createDescription("This directive allows results to be deferred during execution")) -// .directiveLocation(newDirectiveLocation().name(FRAGMENT_SPREAD.name()).build()) -// .directiveLocation(newDirectiveLocation().name(INLINE_FRAGMENT.name()).build()) -// .inputValueDefinition( -// newInputValueDefinition() -// .name("if") -// .description(createDescription("Deferred behaviour is controlled by this argument")) -// .type(newTypeName().name("Boolean").build()) -// .defaultValue(BooleanValue.newBooleanValue(true).build()) -// .build()) -// .inputValueDefinition( -// newInputValueDefinition() -// // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: -// // > `label` must not be provided as a variable. -// // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query -// .name("label") -// .description(createDescription("A unique label that represents the fragment being deferred")) -// .type(newTypeName().name("Boolean").build()) -// .defaultValue(BooleanValue.newBooleanValue(true).build()) -// .build()) -// .build(); } /** * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in * directive that is not available unless it is explicitly put into the schema. */ + @ExperimentalApi public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() .name("defer") .description("This directive allows results to be deferred during execution") From 7c047e23f0dfe721434746d5439cce2cd8205558 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:36:32 +1100 Subject: [PATCH 042/393] Fix assert error message --- .../java/graphql/normalized/incremental/IncrementalNodes.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 4763c6c9d2..6f0bb219c4 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -40,7 +40,7 @@ public DeferLabel getDeferLabel( return new DeferLabel(null); } - Assert.assertTrue(label instanceof String, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); return new DeferLabel((String) label); From 80dc0933bcbe9957c234c4397e47bb14cdcd3cae Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 15:13:19 +1100 Subject: [PATCH 043/393] Add tests for 'if' argument --- ...NormalizedOperationFactoryDeferTest.groovy | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 8c2f844d7a..0b426f7e2d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -1,6 +1,6 @@ package graphql.normalized -import graphql.AssertException + import graphql.ExecutionInput import graphql.GraphQL import graphql.TestUtil @@ -390,6 +390,64 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "'if' argument is respected"() { + given: + + String query = ''' + query q { + dog { + ... @defer(if: false, label: "name-defer") { + name + } + + ... @defer(if: true, label: "another-name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[another-name-defer]', + ] + } + + def "'if' argument with different values on same field and same label"() { + given: + + String query = ''' + query q { + dog { + ... @defer(if: false, label: "name-defer") { + name + } + + ... @defer(if: true, label: "name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer]', + ] + } + private List executeQueryAndPrintTree(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) @@ -416,7 +474,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = field.getDeferExecution().labels - .collect {it.value} + .collect { it.value } .join(",") return " defer[" + deferLabels + "]" From 407efbaf1ee3009141120ed3e25d08c5500eb0b0 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 13 Dec 2023 11:10:20 +1100 Subject: [PATCH 044/393] Add dedicated entry point for defer support --- ...tableNormalizedOperationToAstCompiler.java | 130 +++++++++++++++-- ...izedOperationToAstCompilerDeferTest.groovy | 20 +-- ...ormalizedOperationToAstCompilerTest.groovy | 137 ++++++++++++------ 3 files changed, 220 insertions(+), 67 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 9ae035e975..ffecf5a62a 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.directives.QueryDirectives; import graphql.introspection.Introspection; @@ -124,10 +125,69 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, false); + } + + + /** + * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s, with support for the experimental @defer directive. + *

+ * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable + * OR inlined into the operation text as a graphql literal. + * + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @return a {@link CompilerResult} object + * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, VariablePredicate) + */ + @ExperimentalApi + public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLSchema schema, + @NotNull OperationDefinition.Operation operationKind, + @Nullable String operationName, + @NotNull List topLevelFields, + @Nullable VariablePredicate variablePredicate) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); + } + + /** + * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s, with support for the experimental @defer directive. + *

+ * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable + * OR inlined into the operation text as a graphql literal. + * + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param normalizedFieldToQueryDirectives the map of normalized field to query directives + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @return a {@link CompilerResult} object + * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, Map, VariablePredicate) + */ + @ExperimentalApi + public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLSchema schema, + @NotNull OperationDefinition.Operation operationKind, + @Nullable String operationName, + @NotNull List topLevelFields, + @NotNull Map normalizedFieldToQueryDirectives, + @Nullable VariablePredicate variablePredicate) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, true); + } + + private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, + @NotNull OperationDefinition.Operation operationKind, + @Nullable String operationName, + @NotNull List topLevelFields, + @NotNull Map normalizedFieldToQueryDirectives, + @Nullable VariablePredicate variablePredicate, + boolean deferSupport) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); - List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator); + List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport); SelectionSet selectionSet = new SelectionSet(selections); OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition() @@ -149,7 +209,56 @@ private static List> subselectionsForNormalizedField(GraphQLSchema @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - VariableAccumulator variableAccumulator) { + VariableAccumulator variableAccumulator, + boolean deferSupport) { + if (deferSupport) { + return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); + } else { + return subselectionsForNormalizedFieldNoDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); + } + } + + private static List> subselectionsForNormalizedFieldNoDeferSupport(GraphQLSchema schema, + @NotNull String parentOutputType, + List executableNormalizedFields, + @NotNull Map normalizedFieldToQueryDirectives, + VariableAccumulator variableAccumulator) { + ImmutableList.Builder> selections = ImmutableList.builder(); + + // All conditional fields go here instead of directly to selections, so they can be grouped together + // in the same inline fragment in the output + Map> fieldsByTypeCondition = new LinkedHashMap<>(); + + for (ExecutableNormalizedField nf : executableNormalizedFields) { + if (nf.isConditional(schema)) { + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, false) + .forEach((objectTypeName, field) -> + fieldsByTypeCondition + .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) + .add(field)); + } else { + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, false)); + } + } + + fieldsByTypeCondition.forEach((objectTypeName, fields) -> { + TypeName typeName = newTypeName(objectTypeName).build(); + InlineFragment inlineFragment = newInlineFragment() + .typeCondition(typeName) + .selectionSet(selectionSet(fields)) + .build(); + selections.add(inlineFragment); + }); + + return selections.build(); + } + + + private static List> subselectionsForNormalizedFieldWithDeferSupport(GraphQLSchema schema, + @NotNull String parentOutputType, + List executableNormalizedFields, + @NotNull Map normalizedFieldToQueryDirectives, + VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); // All conditional and deferred fields go here instead of directly to selections, so they can be grouped together @@ -159,7 +268,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) .forEach((objectTypeName, field) -> { if (nf.getDeferExecution() == null) { fieldsByFragmentDetails @@ -175,14 +284,14 @@ private static List> subselectionsForNormalizedField(GraphQLSchema }); } else if (nf.getDeferExecution() != null) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator); + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true); nf.getDeferExecution().getLabels().stream() .map(DeferLabel::getValue) .forEach(label -> fieldsByFragmentDetails .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true)); } } @@ -218,11 +327,12 @@ private static List> subselectionsForNormalizedField(GraphQLSchema private static Map selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, - VariableAccumulator variableAccumulator) { + VariableAccumulator variableAccumulator, + boolean deferSupport) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport)); } return groupedFields; @@ -235,7 +345,8 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, String objectTypeName, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, - VariableAccumulator variableAccumulator) { + VariableAccumulator variableAccumulator, + boolean deferSupport) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); @@ -248,7 +359,8 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, fieldOutputType.getName(), executableNormalizedField.getChildren(), normalizedFieldToQueryDirectives, - variableAccumulator + variableAccumulator, + deferSupport ); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index ac313785a0..96156c8cfb 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -14,7 +14,7 @@ import spock.lang.Specification import static graphql.ExecutionInput.newExecutionInput import static graphql.language.OperationDefinition.Operation.QUERY -import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocument +import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocumentWithDeferSupport class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification { VariablePredicate noVariables = new VariablePredicate() { @@ -74,7 +74,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -102,7 +102,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -133,7 +133,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -167,7 +167,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -198,7 +198,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -233,7 +233,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -270,7 +270,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -299,7 +299,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -347,7 +347,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index dd074ce7a8..05494950b4 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -3,12 +3,13 @@ package graphql.normalized import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables +import graphql.execution.directives.QueryDirectives import graphql.language.AstPrinter -import graphql.language.Field -import graphql.language.OperationDefinition import graphql.language.AstSorter import graphql.language.Document +import graphql.language.Field import graphql.language.IntValue +import graphql.language.OperationDefinition import graphql.language.StringValue import graphql.parser.Parser import graphql.schema.GraphQLSchema @@ -22,8 +23,12 @@ import static graphql.language.OperationDefinition.Operation.MUTATION import static graphql.language.OperationDefinition.Operation.QUERY import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocument +import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocumentWithDeferSupport + +abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specification { + static boolean deferSupport + -class ExecutableNormalizedOperationToAstCompilerTest extends Specification { VariablePredicate noVariables = new VariablePredicate() { @Override boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { @@ -128,7 +133,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -199,7 +204,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -250,7 +255,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -331,7 +336,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -356,6 +361,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } """ } + def "test interface fields with different output types on the implementations 4"() { // Tests we don't consider File as a possible option for parent on animals def schema = TestUtil.schema(""" @@ -422,7 +428,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -517,7 +523,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -584,7 +590,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -641,7 +647,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -699,7 +705,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -766,7 +772,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -864,7 +870,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -962,7 +968,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1029,7 +1035,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1063,7 +1069,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1089,7 +1095,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, "My_Op23", fields, noVariables) + def result = localCompileToDocument(schema, QUERY, "My_Op23", fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1134,7 +1140,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1166,9 +1172,9 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } ''' GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query,["v":123]) + def fields = createNormalizedFields(schema, query, ["v": 123]) when: - def result = compileToDocument(schema, QUERY, null, fields, allVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, allVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1200,7 +1206,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1231,7 +1237,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, SUBSCRIPTION, null, fields, noVariables) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1242,7 +1248,6 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } - def "test query directive"() { def sdl = ''' type Query { @@ -1275,14 +1280,14 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { ''' GraphQLSchema schema = mkSchema(sdl) Document document = new Parser().parse(query) - ExecutableNormalizedOperation eno = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema,document, null,RawVariables.emptyVariables()) + ExecutableNormalizedOperation eno = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) when: - def result = compileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) OperationDefinition operationDefinition = result.document.getDefinitionsOfType(OperationDefinition.class)[0] - def fooField = (Field)operationDefinition.selectionSet.children[0] - def nameField = (Field)fooField.selectionSet.children[0] + def fooField = (Field) operationDefinition.selectionSet.children[0] + def nameField = (Field) fooField.selectionSet.children[0] def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1327,7 +1332,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1372,7 +1377,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1418,7 +1423,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1438,6 +1443,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } ''' } + def "test is conditional when there is only one interface implementation"() { def sdl = ''' type Query { @@ -1468,7 +1474,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1507,7 +1513,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1558,7 +1564,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: // Note: the typename field moves out of a fragment because AFoo is the only impl @@ -1609,7 +1615,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: // Note: the typename field moves out of a fragment because AFoo is the only impl @@ -1659,7 +1665,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = TestUtil.schema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: // Note: the typename field moves out of a fragment because AFoo is the only impl @@ -1695,7 +1701,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, vars) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1727,7 +1733,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, vars) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1759,7 +1765,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, vars) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1789,7 +1795,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1819,7 +1825,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1849,7 +1855,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1879,7 +1885,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1916,7 +1922,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) def vars = result.variables @@ -1953,7 +1959,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1988,7 +1994,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -2031,7 +2037,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, variables) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -2104,7 +2110,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, "named", fields, allVariables) + def result = localCompileToDocument(schema, QUERY, "named", fields, allVariables) def document = result.document def vars = result.variables def ast = AstPrinter.printAst(new AstSorter().sort(document)) @@ -2159,4 +2165,39 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { .wiringFactory(wiringFactory).build() TestUtil.schema(sdl, runtimeWiring) } + + private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( + GraphQLSchema schema, + OperationDefinition.Operation operationKind, + String operationName, + List topLevelFields, + VariablePredicate variablePredicate + ) { + return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate) + } + + private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( + GraphQLSchema schema, + OperationDefinition.Operation operationKind, + String operationName, + List topLevelFields, + Map normalizedFieldToQueryDirectives, + VariablePredicate variablePredicate) { + if (deferSupport) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) + } + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) + } +} + +class ExecutableNormalizedOperationToAstCompilerTestWithDeferSupport extends ExecutableNormalizedOperationToAstCompilerTest { + static { + deferSupport = true + } +} + +class ExecutableNormalizedOperationToAstCompilerTestNoDeferSupport extends ExecutableNormalizedOperationToAstCompilerTest { + static { + deferSupport = false + } } From 17f1fab605381690ff44bd0bf857ee520243015f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 13 Dec 2023 16:25:04 +1100 Subject: [PATCH 045/393] Add dedicated entrypoint with defer support in the ENFOperationFactory --- .../normalized/ExecutableNormalizedField.java | 4 + .../ExecutableNormalizedOperationFactory.java | 106 +++++++++--- ...NormalizedOperationFactoryDeferTest.groovy | 8 +- ...tableNormalizedOperationFactoryTest.groovy | 153 ++++++++++++------ ...izedOperationToAstCompilerDeferTest.groovy | 8 +- 5 files changed, 208 insertions(+), 71 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 73c1df4d0b..cc9f95594b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.Internal; import graphql.Mutable; import graphql.PublicApi; @@ -372,6 +373,7 @@ public String getSingleObjectTypeName() { * TODO Javadoc * @return */ + @ExperimentalApi public DeferExecution getDeferExecution() { return deferExecution; } @@ -670,6 +672,8 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } + + @ExperimentalApi public Builder deferExecution(DeferExecution deferExecution) { this.deferExecution = deferExecution; return this; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 1af327ae39..07d8ca0199 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -173,7 +173,8 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( getOperationResult.fragmentsByName, coercedVariableValues, null, - Options.defaultOptions()); + Options.defaultOptions(), + false); } /** @@ -195,7 +196,8 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( fragments, coercedVariableValues, null, - Options.defaultOptions()); + Options.defaultOptions(), + false); } /** @@ -271,7 +273,40 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW getOperationResult.operationDefinition, getOperationResult.fragmentsByName, rawVariables, - options + options, + false + ); + } + + public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithDeferSupport( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + CoercedVariables coercedVariableValues + ) { + NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); + return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, + getOperationResult.operationDefinition, + getOperationResult.fragmentsByName, + coercedVariableValues, + null, + Options.defaultOptions(), + true); + } + + public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariablesWithDeferSupport(GraphQLSchema graphQLSchema, + Document document, + String operationName, + RawVariables rawVariables, + Options options) { + NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); + + return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, + getOperationResult.operationDefinition, + getOperationResult.fragmentsByName, + rawVariables, + options, + true ); } @@ -279,7 +314,8 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit OperationDefinition operationDefinition, Map fragments, RawVariables rawVariables, - Options options) { + Options options, + boolean deferSupport) { List variableDefinitions = operationDefinition.getVariableDefinitions(); CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, @@ -296,7 +332,8 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit fragments, coercedVariableValues, normalizedVariableValues, - options); + options, + deferSupport); } /** @@ -307,7 +344,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr Map fragments, CoercedVariables coercedVariableValues, @Nullable Map normalizedVariableValues, - Options options) { + Options options, + boolean deferSupport) { FieldCollectorNormalizedQueryParams parameters = FieldCollectorNormalizedQueryParams .newParameters() .fragments(fragments) @@ -349,7 +387,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr captureMergedField, coordinatesToNormalizedFields, 1, - options.getMaxChildrenDepth()); + options.getMaxChildrenDepth(), + deferSupport); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); @@ -374,12 +413,13 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz BiConsumer captureMergedField, ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, - int maxLevel) { + int maxLevel, + boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); } - CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1); + CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); @@ -398,7 +438,8 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz captureMergedField, coordinatesToNormalizedFields, curLevel + 1, - maxLevel); + maxLevel, + deferSupport); } } @@ -446,7 +487,8 @@ public CollectNFResult(Collection children, Immutable public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParams parameters, ExecutableNormalizedField executableNormalizedField, ImmutableList mergedField, - int level) { + int level, + boolean deferSupport) { List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { @@ -472,7 +514,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } @@ -498,7 +540,7 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, false); return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } @@ -508,10 +550,11 @@ private void createNFs(ImmutableList.Builder nfListBu Map> fieldsByName, ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, - ExecutableNormalizedField parent) { + ExecutableNormalizedField parent, + boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); - List commonParentsGroups = groupByCommonParents(fieldsWithSameResultKey); + List commonParentsGroups = groupByCommonParents(fieldsWithSameResultKey, deferSupport); for (CollectedFieldGroup fieldGroup : commonParentsGroups) { ExecutableNormalizedField nf = createNF(parameters, fieldGroup, level, parent); if (nf == null) { @@ -570,7 +613,33 @@ public CollectedFieldGroup(Set fields, Set ob } } - private List groupByCommonParents(Collection fields) { + private List groupByCommonParents(Collection fields, boolean deferSupport) { + if (deferSupport) { + return groupByCommonParentsWithDeferSupport(fields); + } else { + return groupByCommonParentsNoDeferSupport(fields); + } + } + + private List groupByCommonParentsNoDeferSupport(Collection fields) { + ImmutableSet.Builder objectTypes = ImmutableSet.builder(); + for (CollectedField collectedField : fields) { + objectTypes.addAll(collectedField.objectTypes); + } + Set allRelevantObjects = objectTypes.build(); + Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); + if (groupByAstParent.size() == 1) { + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, null)); + } + ImmutableList.Builder result = ImmutableList.builder(); + for (GraphQLObjectType objectType : allRelevantObjects) { + Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), null)); + } + return result.build(); + } + + private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); DeferExecution deferExecution = null; for (CollectedField collectedField : fields) { @@ -616,7 +685,7 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet } else if (selection instanceof InlineFragment) { collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition, deferLabel); } else if (selection instanceof FragmentSpread) { - collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects, deferLabel); + collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects); } } } @@ -647,8 +716,7 @@ public boolean isConcrete() { private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameters, List result, FragmentSpread fragmentSpread, - Set possibleObjects, - DeferLabel deferLabel + Set possibleObjects ) { if (!conditionalNodes.shouldInclude(fragmentSpread, parameters.getCoercedVariableValues(), diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 0b426f7e2d..76750b3cb7 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -453,7 +453,13 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + graphQLSchema, + document, + null, + RawVariables.of(variables), + ExecutableNormalizedOperationFactory.Options.defaultOptions(), + ) return printTreeWithIncrementalExecutionDetails(tree) } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 89ea656b81..72fa48b247 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -27,7 +27,10 @@ import static graphql.language.AstPrinter.printAst import static graphql.parser.Parser.parseValue import static graphql.schema.FieldCoordinates.coordinates -class ExecutableNormalizedOperationFactoryTest extends Specification { +abstract class ExecutableNormalizedOperationFactoryTest extends Specification { + static boolean deferSupport + + def "test"() { String schema = """ type Query{ @@ -112,8 +115,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -199,7 +201,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -279,7 +281,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -330,7 +332,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -373,7 +375,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -423,7 +425,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -486,7 +488,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -532,7 +534,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -576,7 +578,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -620,7 +622,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -652,7 +654,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -703,7 +705,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -753,7 +755,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -792,7 +794,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -836,7 +838,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -876,7 +878,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -924,7 +926,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1027,7 +1029,7 @@ type Dog implements Animal{ def subFooField = (document.getDefinitions()[1] as FragmentDefinition).getSelectionSet().getSelections()[0] as Field ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() expect: @@ -1070,7 +1072,7 @@ type Dog implements Animal{ def idField = petsField.getSelectionSet().getSelections()[0] as Field ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1119,7 +1121,7 @@ type Dog implements Animal{ def typeField = selections[3] as Field ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() expect: @@ -1176,7 +1178,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1219,7 +1221,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -1247,7 +1249,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def normalizedFieldToMergedField = tree.getNormalizedFieldToMergedField() Traverser traverser = Traverser.depthFirst({ it.getChildren() }) List result = new ArrayList<>() @@ -1287,7 +1289,7 @@ type Dog implements Animal{ ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def coordinatesToNormalizedFields = tree.coordinatesToNormalizedFields then: @@ -1386,7 +1388,7 @@ schema { Document document = TestUtil.parseQuery(mutation) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1443,7 +1445,7 @@ schema { // the normalized arg value should be the same regardless of how the value was provided def expectedNormalizedArgValue = [foo: new NormalizedInputValue("String", parseValue('"foo"')), input2: new NormalizedInputValue("Input2", [bar: new NormalizedInputValue("Int", parseValue("123"))])] when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def secondField = topLevelField.getChildren().get(0) def arg1 = secondField.getNormalizedArgument("arg1") @@ -1484,7 +1486,7 @@ schema { def document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) then: def topLevelField = tree.getTopLevelFields().get(0) @@ -1523,7 +1525,7 @@ schema { otherVar: null, ] when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) then: def topLevelField = tree.getTopLevelFields().get(0) @@ -1575,7 +1577,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def arg1 = topLevelField.getNormalizedArgument("arg1") def arg2 = topLevelField.getNormalizedArgument("arg2") @@ -1628,7 +1630,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def arg1 = topLevelField.getNormalizedArgument("arg1") def arg2 = topLevelField.getNormalizedArgument("arg2") @@ -1683,7 +1685,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) then: tree.normalizedFieldToMergedField.size() == 3 @@ -1741,7 +1743,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) then: @@ -1789,7 +1791,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) then: @@ -1865,7 +1867,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -1929,7 +1931,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -1986,7 +1988,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2061,7 +2063,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2123,7 +2125,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2165,7 +2167,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2208,7 +2210,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2251,7 +2253,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2326,7 +2328,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2402,7 +2404,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2464,7 +2466,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) println String.join("\n", printTree(tree)) def printedTree = printTree(tree) @@ -2521,7 +2523,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def printedTree = printTreeAndDirectives(tree) then: @@ -2585,7 +2587,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2638,7 +2640,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2685,7 +2687,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2875,4 +2877,55 @@ fragment personName on Person { then: noExceptionThrown() } + + private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperation( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + CoercedVariables coercedVariableValues + ) { + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + if (deferSupport) { + return dependencyGraph.createExecutableNormalizedOperationWithDeferSupport(graphQLSchema, document, operationName, coercedVariableValues) + } else { + return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues) + } + } + + private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + RawVariables rawVariables + ) { + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + if (deferSupport) { + return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + graphQLSchema, + document, + operationName, + rawVariables, + ExecutableNormalizedOperationFactory.Options.defaultOptions() + ) + } else { + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + operationName, + rawVariables + ) + } + } +} + +class ExecutableNormalizedOperationFactoryTestWithDeferSupport extends ExecutableNormalizedOperationFactoryTest { + static { + deferSupport = true + } +} + +class ExecutableNormalizedOperationFactoryTestNoDeferSupport extends ExecutableNormalizedOperationFactoryTest { + static { + deferSupport = false + } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 96156c8cfb..5eb640508d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -379,7 +379,13 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification Document originalDocument = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) + return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + schema, + originalDocument, + null, + RawVariables.of(variables), + ExecutableNormalizedOperationFactory.Options.defaultOptions() + ) } private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { From 86f9dbe46a87b42d5c3660c14fd6231a94da29f7 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 15 Dec 2023 09:33:08 +1100 Subject: [PATCH 046/393] Add Javadocs --- .../ExecutableNormalizedOperationFactory.java | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 07d8ca0199..120da83ad3 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import graphql.ExperimentalApi; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.collect.ImmutableKit; @@ -278,6 +279,22 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW ); } + + /** + * This will create a runtime representation of the graphql operation that would be executed + * in a runtime sense. + *

+ * This version of the "createExecutableNormalizedOperation" method has support for the `@defer` directive, + * which is still a experimental feature in GraphQL Java. + * + * @param graphQLSchema the schema to be used + * @param document the {@link Document} holding the operation text + * @param operationName the operation name to use + * @param coercedVariableValues the coerced variables to use + * @return a runtime representation of the graphql operation. + * @see ExecutableNormalizedOperationFactory#createExecutableNormalizedOperation(GraphQLSchema, Document, String, CoercedVariables) + */ + @ExperimentalApi public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithDeferSupport( GraphQLSchema graphQLSchema, Document document, @@ -294,11 +311,27 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW true); } + /** + * This will create a runtime representation of the graphql operation that would be executed + * in a runtime sense. + * + *

+ * This version of the "createExecutableNormalizedOperationWithRawVariables" method has support for the `@defer` + * directive, which is still a experimental feature in GraphQL Java. + * + * @param graphQLSchema the schema to be used + * @param document the {@link Document} holding the operation text + * @param operationName the operation name to use + * @param rawVariables the raw variables that have not yet been coerced + * @param options the {@link Options} to use for parsing + * @return a runtime representation of the graphql operation. + */ + @ExperimentalApi public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariablesWithDeferSupport(GraphQLSchema graphQLSchema, - Document document, - String operationName, - RawVariables rawVariables, - Options options) { + Document document, + String operationName, + RawVariables rawVariables, + Options options) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, From 0f4abf4565822148253b1bdbe2f058338d26f329 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 15 Dec 2023 10:15:48 +1100 Subject: [PATCH 047/393] Lots of small adjustments --- src/main/java/graphql/Directives.java | 7 ++--- .../normalized/ExecutableNormalizedField.java | 17 ++++------- ...tableNormalizedOperationToAstCompiler.java | 7 +++-- .../normalized/incremental/DeferLabel.java | 5 ++++ ...NormalizedOperationFactoryDeferTest.groovy | 29 +++++++++++++++++++ 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 98ca5b5625..fe207f47c8 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -79,8 +79,8 @@ public class Directives { } /** - * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in - * directive that is not available unless it is explicitly put into the schema. + * The @defer directive can be used to defer sending data for a fragment until later in the query. + * This is an opt-in directive that is not available unless it is explicitly put into the schema. */ @ExperimentalApi public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() @@ -94,9 +94,6 @@ public class Directives { .defaultValueLiteral(BooleanValue.newBooleanValue(true).build()) ) .argument(newArgument() - // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: - // > `label` must not be provided as a variable. - // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query .name("label") .type(GraphQLString) .description("A unique label that represents the fragment being deferred") diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index cc9f95594b..12ab96e87d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -265,7 +265,6 @@ public void clearChildren() { * WARNING: This is not always the key in the execution result, because of possible field aliases. * * @return the name of this {@link ExecutableNormalizedField} - * * @see #getResultKey() * @see #getAlias() */ @@ -275,7 +274,6 @@ public String getName() { /** * @return the same value as {@link #getName()} - * * @see #getResultKey() * @see #getAlias() */ @@ -288,7 +286,6 @@ public String getFieldName() { * This is either a field alias or the value of {@link #getName()} * * @return the result key for this {@link ExecutableNormalizedField}. - * * @see #getName() */ public String getResultKey() { @@ -300,7 +297,6 @@ public String getResultKey() { /** * @return the field alias used or null if there is none - * * @see #getResultKey() * @see #getName() */ @@ -319,7 +315,6 @@ public ImmutableList getAstArguments() { * Returns an argument value as a {@link NormalizedInputValue} which contains its type name and its current value * * @param name the name of the argument - * * @return an argument value */ public NormalizedInputValue getNormalizedArgument(String name) { @@ -368,12 +363,15 @@ public String getSingleObjectTypeName() { return objectTypeNames.iterator().next(); } - /** - * TODO Javadoc - * @return + * Returns an object containing the details about the defer aspect of this field's execution. + *

+ * "null" is returned when this field is not supposed to be deferred. + * + * @return details about the defer execution */ @ExperimentalApi + @Nullable public DeferExecution getDeferExecution() { return deferExecution; } @@ -427,7 +425,6 @@ public List getChildren() { * Returns the list of child fields that would have the same result key * * @param resultKey the result key to check - * * @return a list of all direct {@link ExecutableNormalizedField} children with the specified result key */ public List getChildrenWithSameResultKey(String resultKey) { @@ -448,7 +445,6 @@ public List getChildren(int includingRelativeLevel) { * This returns the child fields that can be used if the object is of the specified object type * * @param objectTypeName the object type - * * @return a list of child fields that would apply to that object type */ public List getChildren(String objectTypeName) { @@ -581,7 +577,6 @@ public static Builder newNormalizedField() { * Allows this {@link ExecutableNormalizedField} to be transformed via a {@link Builder} consumer callback * * @param builderConsumer the consumer given a builder - * * @return a new transformed {@link ExecutableNormalizedField} */ public ExecutableNormalizedField transform(Consumer builderConsumer) { diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index ffecf5a62a..63bc5a8bf5 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; +import graphql.Directives; import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.directives.QueryDirectives; @@ -305,7 +306,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor } if (typeAndDeferPair.deferLabel != null) { - Directive.Builder deferBuilder = Directive.newDirective().name("defer"); + Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); if (typeAndDeferPair.deferLabel.getValue() != null) { deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferLabel.getValue())).build()); @@ -471,7 +472,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, return Assert.assertShouldNeverHappen("Unknown operation kind " + operationKind); } - // TODO: This name is terrible + /** + * Represents important execution details that can be associated with a fragment. + */ private static class ExecutionFragmentDetails { private final String typeName; private final DeferLabel deferLabel; diff --git a/src/main/java/graphql/normalized/incremental/DeferLabel.java b/src/main/java/graphql/normalized/incremental/DeferLabel.java index d965e5636d..4c6249bd62 100644 --- a/src/main/java/graphql/normalized/incremental/DeferLabel.java +++ b/src/main/java/graphql/normalized/incremental/DeferLabel.java @@ -1,11 +1,16 @@ package graphql.normalized.incremental; +import graphql.ExperimentalApi; + import javax.annotation.Nullable; import java.util.Objects; /** * Holds the value of the 'label' argument of a defer directive. + *

+ * 'value' can be 'null', as 'label' is not a required argument on the '@defer' directive. */ +@ExperimentalApi public class DeferLabel { private final String value; diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 76750b3cb7..3d5f809fa1 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -419,6 +419,35 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "'if' argument is respected when value is passed through variable"() { + given: + + String query = ''' + query q($if1: Boolean, $if2: Boolean) { + dog { + ... @defer(if: $if1, label: "name-defer") { + name + } + + ... @defer(if: $if2, label: "another-name-defer") { + name + } + } + } + + ''' + + Map variables = [if1: false, if2: true] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[another-name-defer]', + ] + } + def "'if' argument with different values on same field and same label"() { given: From 444f775ec13736952945f97dc7c75d41fd107717 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 15 Dec 2023 17:32:25 +1100 Subject: [PATCH 048/393] Add test case for multiple defers at the same level --- ...izedOperationToAstCompilerDeferTest.groovy | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 5eb640508d..fe8e98116a 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -374,6 +374,48 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "multiple defers at the same level are preserved"() { + String query = """ + query q { + dog { + ... @defer { + name + } + ... @defer { + breed + } + ... @defer { + owner { + firstname + } + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + ... @defer { + name + } + ... @defer { + breed + } + ... @defer { + owner { + firstname + } + } + } + } +''' + } + private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) From 32881a30b1fde07f2fe24fd27a335804fd9a62ea Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 18 Dec 2023 15:47:31 +1100 Subject: [PATCH 049/393] Moving over to new implementation: changed method signatures --- .../java/graphql/DeferredExecutionResult.java | 16 ++ .../graphql/DeferredExecutionResultImpl.java | 68 +++++++++ .../graphql/execution/FieldCollector.java | 2 +- .../graphql/execution/defer/DeferSupport.java | 82 ++++++++++ .../graphql/execution/defer/DeferredCall.java | 43 ++++++ .../execution/defer/DeferredErrorSupport.java | 31 ++++ .../DeferredFieldInstrumentationContext.java | 12 ++ ...nstrumentationDeferredFieldParameters.java | 25 ++++ .../ExecutableNormalizedOperation.java | 6 +- .../ExecutableNormalizedOperationFactory.java | 77 +++++----- ...tableNormalizedOperationToAstCompiler.java | 84 +++++------ .../incremental/DeferExecution.java | 34 +---- .../incremental/IncrementalNodes.java | 36 ++++- .../execution/FieldCollectorTest.groovy | 140 +++++++++++++++++- ...izedOperationToAstCompilerDeferTest.groovy | 20 +-- ...ormalizedOperationToAstCompilerTest.groovy | 2 +- 16 files changed, 547 insertions(+), 131 deletions(-) create mode 100644 src/main/java/graphql/DeferredExecutionResult.java create mode 100644 src/main/java/graphql/DeferredExecutionResultImpl.java create mode 100644 src/main/java/graphql/execution/defer/DeferSupport.java create mode 100644 src/main/java/graphql/execution/defer/DeferredCall.java create mode 100644 src/main/java/graphql/execution/defer/DeferredErrorSupport.java create mode 100644 src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java create mode 100644 src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java diff --git a/src/main/java/graphql/DeferredExecutionResult.java b/src/main/java/graphql/DeferredExecutionResult.java new file mode 100644 index 0000000000..18eb23c74a --- /dev/null +++ b/src/main/java/graphql/DeferredExecutionResult.java @@ -0,0 +1,16 @@ +package graphql; + +import java.util.List; + +/** + * Results that come back from @defer fields have an extra path property that tells you where + * that deferred result came in the original query + */ +@PublicApi +public interface DeferredExecutionResult extends ExecutionResult { + + /** + * @return the execution path of this deferred result in the original query + */ + List getPath(); +} diff --git a/src/main/java/graphql/DeferredExecutionResultImpl.java b/src/main/java/graphql/DeferredExecutionResultImpl.java new file mode 100644 index 0000000000..b5a57eadd4 --- /dev/null +++ b/src/main/java/graphql/DeferredExecutionResultImpl.java @@ -0,0 +1,68 @@ +package graphql; + +import graphql.execution.ResultPath; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static graphql.Assert.assertNotNull; + +/** + * Results that come back from @defer fields have an extra path property that tells you where + * that deferred result came in the original query + */ +@PublicApi +public class DeferredExecutionResultImpl extends ExecutionResultImpl implements DeferredExecutionResult { + + private final List path; + + private DeferredExecutionResultImpl(List path, ExecutionResultImpl executionResult) { + super(executionResult); + this.path = assertNotNull(path); + } + + /** + * @return the execution path of this deferred result in the original query + */ + public List getPath() { + return path; + } + + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + map.put("path", path); + return map; + } + + public static Builder newDeferredExecutionResult() { + return new Builder(); + } + + public static class Builder { + private List path = Collections.emptyList(); + private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); + + public Builder path(ResultPath path) { + this.path = assertNotNull(path).toList(); + return this; + } + + public Builder from(ExecutionResult executionResult) { + builder.from(executionResult); + return this; + } + + public Builder addErrors(List errors) { + builder.addErrors(errors); + return this; + } + + public DeferredExecutionResult build() { + ExecutionResultImpl build = (ExecutionResultImpl) builder.build(); + return new DeferredExecutionResultImpl(path, build); + } + } +} diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index a6f1310a8c..8fae8a3afb 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -25,7 +25,7 @@ /** * A field collector can iterate over field selection sets and build out the sub fields that have been selected, - * expanding named and inline fragments as it goes.s + * expanding named and inline fragments as it goes. */ @Internal public class FieldCollector { diff --git a/src/main/java/graphql/execution/defer/DeferSupport.java b/src/main/java/graphql/execution/defer/DeferSupport.java new file mode 100644 index 0000000000..3bc777b8c5 --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferSupport.java @@ -0,0 +1,82 @@ +package graphql.execution.defer; + +import graphql.DeferredExecutionResult; +import graphql.Directives; +import graphql.ExecutionResult; +import graphql.Internal; +import graphql.execution.MergedField; +import graphql.execution.ValuesResolver; +import graphql.execution.reactive.SingleSubscriberPublisher; +import graphql.language.Directive; +import graphql.language.Field; +import org.reactivestreams.Publisher; + +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +import static graphql.Directives.*; + +/** + * This provides support for @defer directives on fields that mean that results will be sent AFTER + * the main result is sent via a Publisher stream. + */ +@Internal +public class DeferSupport { + + private final AtomicBoolean deferDetected = new AtomicBoolean(false); + private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); + private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); +// private final ValuesResolver valuesResolver = new ValuesResolver(); + +// public boolean checkForDeferDirective(MergedField currentField, Map variables) { +// for (Field field : currentField.getFields()) { +// Directive directive = field.getDirective(DeferDirective.getName()); +// if (directive != null) { +// Map argumentValues = valuesResolver.getArgumentValues(DeferDirective.getArguments(), directive.getArguments(), variables); +// return (Boolean) argumentValues.get("if"); +// } +// } +// return false; +// } +// +// @SuppressWarnings("FutureReturnValueIgnored") +// private void drainDeferredCalls() { +// if (deferredCalls.isEmpty()) { +// publisher.noMoreData(); +// return; +// } +// DeferredCall deferredCall = deferredCalls.pop(); +// CompletableFuture future = deferredCall.invoke(); +// future.whenComplete((executionResult, exception) -> { +// if (exception != null) { +// publisher.offerError(exception); +// return; +// } +// publisher.offer(executionResult); +// drainDeferredCalls(); +// }); +// } +// +// public void enqueue(DeferredCall deferredCall) { +// deferDetected.set(true); +// deferredCalls.offer(deferredCall); +// } +// +// public boolean isDeferDetected() { +// return deferDetected.get(); +// } +// +// /** +// * When this is called the deferred execution will begin +// * +// * @return the publisher of deferred results +// */ +// public Publisher startDeferredCalls() { +// drainDeferredCalls(); +// return publisher; +// } +} diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java new file mode 100644 index 0000000000..b52e124d5b --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -0,0 +1,43 @@ +package graphql.execution.defer; + +import graphql.DeferredExecutionResult; +import graphql.DeferredExecutionResultImpl; +import graphql.ExecutionResult; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.execution.ResultPath; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * This represents a deferred call (aka @defer) to get an execution result sometime after + * the initial query has returned + */ +@Internal +public class DeferredCall { + private final ResultPath path; + private final Supplier> call; + private final DeferredErrorSupport errorSupport; + + public DeferredCall(ResultPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { + this.path = path; + this.call = call; + this.errorSupport = deferredErrorSupport; + } + + CompletableFuture invoke() { + CompletableFuture future = call.get(); + return future.thenApply(this::transformToDeferredResult); + } + + private DeferredExecutionResult transformToDeferredResult(ExecutionResult executionResult) { + List errorsEncountered = errorSupport.getErrors(); + DeferredExecutionResultImpl.Builder builder = DeferredExecutionResultImpl.newDeferredExecutionResult().from(executionResult); + return builder + .addErrors(errorsEncountered) + .path(path) + .build(); + } +} diff --git a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java new file mode 100644 index 0000000000..3ef4f34c5d --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java @@ -0,0 +1,31 @@ +package graphql.execution.defer; + +import graphql.ExceptionWhileDataFetching; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.execution.ExecutionStrategyParameters; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This captures errors that occur while a deferred call is being made + */ +@Internal +public class DeferredErrorSupport { + + private final List errors = new CopyOnWriteArrayList<>(); + + public void onFetchingException(ExecutionStrategyParameters parameters, Throwable e) { + ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(parameters.getPath(), e, parameters.getField().getSingleField().getSourceLocation()); + onError(error); + } + + public void onError(GraphQLError gError) { + errors.add(gError); + } + + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java new file mode 100644 index 0000000000..39de62be19 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java @@ -0,0 +1,12 @@ +package graphql.execution.instrumentation; + +import graphql.ExecutionResult; +import graphql.execution.FieldValueInfo; + +public interface DeferredFieldInstrumentationContext extends InstrumentationContext { + + default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + + } + +} diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java new file mode 100644 index 0000000000..e71d104438 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java @@ -0,0 +1,25 @@ +package graphql.execution.instrumentation.parameters; + +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.ExecutionStrategyParameters; + +import java.util.function.Supplier; + +/** + * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods + */ +public class InstrumentationDeferredFieldParameters extends InstrumentationFieldParameters { + + private final ExecutionStrategyParameters executionStrategyParameters; + + + public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, ExecutionStrategyParameters executionStrategyParameters) { + super(executionContext, executionStepInfo); + this.executionStrategyParameters = executionStrategyParameters; + } + + public ExecutionStrategyParameters getExecutionStrategyParameters() { + return executionStrategyParameters; + } +} diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index ce50c9931b..83cebdb77f 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -8,6 +8,7 @@ import graphql.execution.directives.QueryDirectives; import graphql.language.Field; import graphql.language.OperationDefinition; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLFieldsContainer; @@ -30,6 +31,7 @@ public class ExecutableNormalizedOperation { private final ImmutableListMultimap fieldToNormalizedField; private final Map normalizedFieldToMergedField; private final Map normalizedFieldToQueryDirectives; + private final ImmutableListMultimap normalizedFieldToDeferExecution; private final ImmutableListMultimap coordinatesToNormalizedFields; public ExecutableNormalizedOperation( @@ -39,7 +41,8 @@ public ExecutableNormalizedOperation( ImmutableListMultimap fieldToNormalizedField, Map normalizedFieldToMergedField, Map normalizedFieldToQueryDirectives, - ImmutableListMultimap coordinatesToNormalizedFields + ImmutableListMultimap coordinatesToNormalizedFields, + ImmutableListMultimap normalizedFieldToDeferExecution ) { this.operation = operation; this.operationName = operationName; @@ -48,6 +51,7 @@ public ExecutableNormalizedOperation( this.normalizedFieldToMergedField = normalizedFieldToMergedField; this.normalizedFieldToQueryDirectives = normalizedFieldToQueryDirectives; this.coordinatesToNormalizedFields = coordinatesToNormalizedFields; + this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } /** diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 120da83ad3..081146202b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -28,7 +28,6 @@ import graphql.language.SelectionSet; import graphql.language.VariableDefinition; import graphql.normalized.incremental.DeferExecution; -import graphql.normalized.incremental.DeferLabel; import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; @@ -434,7 +433,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr fieldToNormalizedField.build(), normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), - coordinatesToNormalizedFields.build() + coordinatesToNormalizedFields.build(), + collectFromOperationResult.normalizedFieldToDeferExecution ); } @@ -509,10 +509,12 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; + private final ImmutableListMultimap normalizedFieldToDeferExecution; - public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields) { + public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields, ImmutableListMultimap normalizedFieldToDeferExecution) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; + this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -525,7 +527,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableListMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -546,10 +548,11 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); + ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -572,10 +575,11 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); + ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, false); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, false); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } private void createNFs(ImmutableList.Builder nfListBuilder, @@ -584,6 +588,7 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, + ImmutableListMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -630,19 +635,18 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p .fieldName(fieldName) .level(level) .parent(parent) - .deferExecution(collectedFieldGroup.deferExecution) .build(); } private static class CollectedFieldGroup { Set objectTypes; Set fields; - DeferExecution deferExecution; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, DeferExecution deferExecution) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; - this.deferExecution = deferExecution; + this.deferExecutions = deferExecutions; } } @@ -674,32 +678,29 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - DeferExecution deferExecution = null; + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferLabel collectedDeferLabel = collectedField.deferLabel; - - if (collectedDeferLabel == null) { - continue; - } + DeferExecution collectedDeferExecution = collectedField.deferExecution; - if (deferExecution == null) { - deferExecution = new DeferExecution(); + if (collectedDeferExecution != null) { + deferExecutionsBuilder.add(collectedDeferExecution); } - - deferExecution.addLabel(collectedDeferLabel); } Set allRelevantObjects = objectTypes.build(); + Set deferExecutions = deferExecutionsBuilder.build(); + Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecution)); + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecutions)); } ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecution)); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecutions)); } return result.build(); } @@ -710,13 +711,13 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferLabel deferLabel + DeferExecution deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { - collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition, deferLabel); + collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition, deferExecution); } else if (selection instanceof InlineFragment) { - collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition, deferLabel); + collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition); } else if (selection instanceof FragmentSpread) { collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects); } @@ -727,14 +728,13 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; + DeferExecution deferExecution; - DeferLabel deferLabel; - - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferLabel deferLabel) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; - this.deferLabel = deferLabel; + this.deferExecution = deferExecution; } public boolean isAbstract() { @@ -766,11 +766,11 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); + collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -778,8 +778,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter List result, InlineFragment inlineFragment, Set possibleObjects, - GraphQLCompositeType astTypeCondition, - DeferLabel deferLabel + GraphQLCompositeType astTypeCondition ) { if (!conditionalNodes.shouldInclude(inlineFragment, parameters.getCoercedVariableValues(), parameters.getGraphQLSchema(), parameters.getGraphQLContext())) { return; @@ -793,9 +792,9 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter } - DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); - collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); + collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } private void collectField(FieldCollectorNormalizedQueryParams parameters, @@ -803,7 +802,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferLabel deferLabel + DeferExecution deferExecution ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), @@ -815,7 +814,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, if (possibleObjectTypes.isEmpty()) { return; } - result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferLabel)); + result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferExecution)); } private Set narrowDownPossibleObjects(Set currentOnes, diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 63bc5a8bf5..1737eb9dec 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,7 +23,7 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferLabel; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -126,7 +126,7 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, false); + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, null); } @@ -149,8 +149,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull OperationDefinition.Operation operationKind, @Nullable String operationName, @NotNull List topLevelFields, - @Nullable VariablePredicate variablePredicate) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); + @Nullable VariablePredicate variablePredicate, + @Nullable Map normalizedFieldToDeferExecution) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); } /** @@ -174,8 +175,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, - @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, true); + @Nullable VariablePredicate variablePredicate, + @Nullable Map normalizedFieldToDeferExecution) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); } private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -184,11 +186,11 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - boolean deferSupport) { + @Nullable Map normalizedFieldToDeferExecution) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); - List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport); + List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); SelectionSet selectionSet = new SelectionSet(selections); OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition() @@ -211,9 +213,9 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - boolean deferSupport) { - if (deferSupport) { - return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); + @Nullable Map normalizedFieldToDeferExecution) { + if (normalizedFieldToDeferExecution != null) { + return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); } else { return subselectionsForNormalizedFieldNoDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); } @@ -232,13 +234,13 @@ private static List> subselectionsForNormalizedFieldNoDeferSupport( for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, false) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false) .forEach((objectTypeName, field) -> fieldsByTypeCondition .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, false)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false)); } } @@ -259,6 +261,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, + @NotNull Map normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -268,31 +271,24 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { + DeferExecution deferExecution = normalizedFieldToDeferExecution.get(nf); + if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true) .forEach((objectTypeName, field) -> { - if (nf.getDeferExecution() == null) { - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) - .add(field); - } else { - nf.getDeferExecution().getLabels().stream() - .map(DeferLabel::getValue) - .forEach(label -> fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, new DeferLabel(label)), ignored -> new ArrayList<>()) - .add(field)); - } + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) + .add(field); }); - } else if (nf.getDeferExecution() != null) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true); - nf.getDeferExecution().getLabels().stream() - .map(DeferLabel::getValue) - .forEach(label -> fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) - .add(field)); + } else if (deferExecution != null) { + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true); + + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, deferExecution), ignored -> new ArrayList<>()) + .add(field); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true)); } } @@ -305,11 +301,11 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor fragmentBuilder.typeCondition(typeName); } - if (typeAndDeferPair.deferLabel != null) { + if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferLabel.getValue() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferLabel.getValue())).build()); + if (typeAndDeferPair.deferExecution.getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -329,11 +325,12 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, + @Nullable Map normalizedFieldToDeferExecution, boolean deferSupport) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, deferSupport)); } return groupedFields; @@ -347,6 +344,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, + @Nullable Map normalizedFieldToDeferExecution, boolean deferSupport) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { @@ -361,7 +359,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, executableNormalizedField.getChildren(), normalizedFieldToQueryDirectives, variableAccumulator, - deferSupport + normalizedFieldToDeferExecution ); } @@ -477,11 +475,11 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferLabel deferLabel; + private final DeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, DeferLabel deferLabel) { + public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { this.typeName = typeName; - this.deferLabel = deferLabel; + this.deferExecution = deferExecution; } @Override @@ -489,12 +487,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ExecutionFragmentDetails that = (ExecutionFragmentDetails) o; - return Objects.equals(typeName, that.typeName) && Objects.equals(deferLabel, that.deferLabel); + return Objects.equals(typeName, that.typeName) && Objects.equals(deferExecution, that.deferExecution); } @Override public int hashCode() { - return Objects.hash(typeName, deferLabel); + return Objects.hash(typeName, deferExecution); } } } diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 7754d6de83..f201be211b 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -2,39 +2,17 @@ import graphql.ExperimentalApi; -import java.util.LinkedHashSet; -import java.util.Set; +import javax.annotation.Nullable; -/** - * This class holds information about the defer execution of an ENF. - *

- * Given that an ENF can be linked with numerous defer labels, a {@link DeferExecution} instance comprises a - * collection of these labels. - *

- * For example, this query: - *

- *   query q {
- *    dog {
- *      ... @defer(label: "name-defer") {
- *        name
- *      }
- *      ... @defer(label: "another-name-defer") {
- *        name
- *      }
- *    }
- *  }
- *  
- * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" - */ @ExperimentalApi public class DeferExecution { - private final Set labels = new LinkedHashSet<>(); + private final String label; - public void addLabel(DeferLabel label) { - this.labels.add(label); + public DeferExecution(@Nullable String label) { + this.label = label; } - public Set getLabels() { - return this.labels; + public String getLabel() { + return label; } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 6f0bb219c4..4d09d5ca75 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -7,7 +7,6 @@ import graphql.execution.ValuesResolver; import graphql.language.Directive; import graphql.language.NodeUtil; -import graphql.schema.GraphQLSchema; import java.util.List; import java.util.Locale; @@ -30,13 +29,13 @@ public DeferLabel getDeferLabel( Object flag = argumentValues.get("if"); Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); - if(!((Boolean) flag)) { + if (!((Boolean) flag)) { return null; } Object label = argumentValues.get("label"); - if(label == null) { + if (label == null) { return new DeferLabel(null); } @@ -48,4 +47,35 @@ public DeferLabel getDeferLabel( return null; } + + public DeferExecution getDeferExecution( + Map variables, + List directives + ) { + Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); + + if (deferDirective != null) { + Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); + + Object flag = argumentValues.get("if"); + Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + if (!((Boolean) flag)) { + return null; + } + + Object label = argumentValues.get("label"); + + if (label == null) { + return new DeferExecution(null); + } + + Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); + + return new DeferExecution((String) label); + + } + + return null; + } } diff --git a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy index 1fa8f360e8..6460512543 100644 --- a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy +++ b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy @@ -21,7 +21,7 @@ class FieldCollectorTest extends Specification { type Query { bar1: String bar2: String - } + } """) def objectType = schema.getType("Query") as GraphQLObjectType FieldCollector fieldCollector = new FieldCollector() @@ -48,12 +48,12 @@ class FieldCollectorTest extends Specification { type Query{ bar1: String bar2: Test - } + } interface Test { - fieldOnInterface: String - } + fieldOnInterface: String + } type TestImpl implements Test { - fieldOnInterface: String + fieldOnInterface: String } """) def object = schema.getType("TestImpl") as GraphQLObjectType @@ -73,6 +73,136 @@ class FieldCollectorTest extends Specification { then: result.getSubField('fieldOnInterface').getFields() == [interfaceField] + } + + def "collect fields that are merged together - one of the fields is on an inline fragment "() { + def schema = TestUtil.schema(""" + type Query { + echo: String + } +""") + + Document document = new Parser().parseDocument(""" + { + echo + ... on Query { + echo + } + } + +""") + + def object = schema.getType("TestImpl") as GraphQLObjectType + FieldCollector fieldCollector = new FieldCollector() + FieldCollectorParameters fieldCollectorParameters = newParameters() + .schema(schema) + .objectType(object) + .build() + + def selectionSet = ((OperationDefinition) document.children[0]).selectionSet + + when: + def result = fieldCollector.collectFields(fieldCollectorParameters, selectionSet) + + then: + result.size() == 1 + result.getSubField('echo').fields.size() == 1 + } + + def "collect fields that are merged together - fields have different selection sets "() { + def schema = TestUtil.schema(""" + type Query { + me: Me + } + + type Me { + firstname: String + lastname: String + } +""") + + Document document = new Parser().parseDocument(""" + { + me { + firstname + } + me { + lastname + } + } + +""") + + def object = schema.getType("TestImpl") as GraphQLObjectType + FieldCollector fieldCollector = new FieldCollector() + FieldCollectorParameters fieldCollectorParameters = newParameters() + .schema(schema) + .objectType(object) + .build() + + def selectionSet = ((OperationDefinition) document.children[0]).selectionSet + + when: + def result = fieldCollector.collectFields(fieldCollectorParameters, selectionSet) + + then: + result.size() == 1 + + def meField = result.getSubField('me') + + meField.fields.size() == 2 + + meField.fields[0].selectionSet.selections.size() == 1 + meField.fields[0].selectionSet.selections[0].name == "firstname" + + meField.fields[1].selectionSet.selections.size() == 1 + meField.fields[1].selectionSet.selections[0].name == "lastname" + } + + def "collect fields that are merged together - fields have different directives"() { + def schema = TestUtil.schema(""" + directive @one on FIELD + directive @two on FIELD + + type Query { + echo: String + } +""") + + Document document = new Parser().parseDocument(""" + { + echo @one + echo @two + } + +""") + + def object = schema.getType("TestImpl") as GraphQLObjectType + FieldCollector fieldCollector = new FieldCollector() + FieldCollectorParameters fieldCollectorParameters = newParameters() + .schema(schema) + .objectType(object) + .build() + + def selectionSet = ((OperationDefinition) document.children[0]).selectionSet + + when: + def result = fieldCollector.collectFields(fieldCollectorParameters, selectionSet) + + then: + result.size() == 1 + + def echoField = result.getSubField('echo') + + echoField.fields.size() == 2 + + echoField.fields[0].name == "echo" + echoField.fields[0].directives.size() == 1 + echoField.fields[0].directives[0].name == "one" + + echoField.fields[1].name == "echo" + echoField.fields[1].directives.size() == 1 + echoField.fields[1].directives[0].name == "two" } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index fe8e98116a..ac7f58b9aa 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -74,7 +74,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -102,7 +102,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -133,7 +133,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -167,7 +167,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -198,7 +198,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -233,7 +233,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -270,7 +270,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -299,7 +299,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -347,7 +347,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -395,7 +395,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 05494950b4..ec88d85dfc 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2184,7 +2184,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat Map normalizedFieldToQueryDirectives, VariablePredicate variablePredicate) { if (deferSupport) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, [:]) } return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } From 8fe9e38793b2e76379930bd7ee7effb9e436092c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 20 Dec 2023 11:42:11 +1100 Subject: [PATCH 050/393] Conclude refactoring that moves defer support out of ENFs and into ENOs --- src/main/java/graphql/ExperimentalApi.java | 6 +- .../normalized/ExecutableNormalizedField.java | 28 --- .../ExecutableNormalizedOperation.java | 10 + .../ExecutableNormalizedOperationFactory.java | 212 ++++++++++-------- ...tableNormalizedOperationToAstCompiler.java | 58 +++-- .../incremental/DeferExecution.java | 20 +- .../normalized/incremental/DeferLabel.java | 39 ---- .../incremental/IncrementalNodes.java | 40 +--- ...NormalizedOperationFactoryDeferTest.groovy | 25 ++- ...tableNormalizedOperationFactoryTest.groovy | 33 +-- ...izedOperationToAstCompilerDeferTest.groovy | 171 ++++++++++---- ...ormalizedOperationToAstCompilerTest.groovy | 49 ++-- 12 files changed, 376 insertions(+), 315 deletions(-) delete mode 100644 src/main/java/graphql/normalized/incremental/DeferLabel.java diff --git a/src/main/java/graphql/ExperimentalApi.java b/src/main/java/graphql/ExperimentalApi.java index c405ec10cf..991932b2d4 100644 --- a/src/main/java/graphql/ExperimentalApi.java +++ b/src/main/java/graphql/ExperimentalApi.java @@ -12,9 +12,9 @@ /** * This represents code that the graphql-java project considers experimental API and while our intention is that it will - * progress to be {@link PublicApi}, its existence, signature of behavior may change between releases. - * - * In general unnecessary changes will be avoided but you should not depend on experimental classes being stable + * progress to be {@link PublicApi}, its existence, signature or behavior may change between releases. + *

+ * In general unnecessary changes will be avoided, but you should not depend on experimental classes being stable. */ @Retention(RetentionPolicy.RUNTIME) @Target(value = {CONSTRUCTOR, METHOD, TYPE, FIELD}) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 12ab96e87d..01f0d3fddd 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -3,14 +3,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; -import graphql.ExperimentalApi; import graphql.Internal; import graphql.Mutable; import graphql.PublicApi; import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -65,8 +63,6 @@ public class ExecutableNormalizedField { private final String fieldName; private final int level; - private final DeferExecution deferExecution; - private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; this.resolvedArguments = builder.resolvedArguments; @@ -77,7 +73,6 @@ private ExecutableNormalizedField(Builder builder) { this.children = builder.children; this.level = builder.level; this.parent = builder.parent; - this.deferExecution = builder.deferExecution; } /** @@ -363,19 +358,6 @@ public String getSingleObjectTypeName() { return objectTypeNames.iterator().next(); } - /** - * Returns an object containing the details about the defer aspect of this field's execution. - *

- * "null" is returned when this field is not supposed to be deferred. - * - * @return details about the defer execution - */ - @ExperimentalApi - @Nullable - public DeferExecution getDeferExecution() { - return deferExecution; - } - /** * @return a helper method show field details */ @@ -596,8 +578,6 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private DeferExecution deferExecution; - private Builder() { } @@ -606,7 +586,6 @@ private Builder(ExecutableNormalizedField existing) { this.normalizedArguments = existing.normalizedArguments; this.astArguments = existing.astArguments; this.resolvedArguments = existing.resolvedArguments; - this.deferExecution = existing.deferExecution; this.objectTypeNames = new LinkedHashSet<>(existing.getObjectTypeNames()); this.fieldName = existing.getFieldName(); this.children = new ArrayList<>(existing.children); @@ -667,13 +646,6 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - - @ExperimentalApi - public Builder deferExecution(DeferExecution deferExecution) { - this.deferExecution = deferExecution; - return this; - } - public ExecutableNormalizedField build() { return new ExecutableNormalizedField(this); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index 83cebdb77f..9bfef00b26 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableListMultimap; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.MergedField; import graphql.execution.ResultPath; @@ -130,6 +131,15 @@ public Map getNormalizedFieldToQuery } + /** + * @return a map of {@link ExecutableNormalizedField} to its {@link DeferExecution} + */ + @ExperimentalApi + public ImmutableListMultimap getNormalizedFieldToDeferExecution() { + return normalizedFieldToDeferExecution; + } + + /** * This looks up the {@link QueryDirectives} associated with the given {@link ExecutableNormalizedField} * diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 081146202b..33682b176b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import graphql.Assert; import graphql.ExperimentalApi; import graphql.GraphQLContext; import graphql.PublicApi; @@ -49,8 +50,12 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; import static graphql.Assert.assertShouldNeverHappen; @@ -61,6 +66,7 @@ import static graphql.util.FpKit.intersection; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toSet; /** * This factory can create a {@link ExecutableNormalizedOperation} which represents what would be executed @@ -73,19 +79,24 @@ public static class Options { private final Locale locale; private final int maxChildrenDepth; + private final boolean deferSupport; + private Options(GraphQLContext graphQLContext, Locale locale, - int maxChildrenDepth) { + int maxChildrenDepth, + boolean deferSupport) { this.graphQLContext = graphQLContext; this.locale = locale; this.maxChildrenDepth = maxChildrenDepth; + this.deferSupport = deferSupport; } public static Options defaultOptions() { return new Options( GraphQLContext.getDefault(), Locale.getDefault(), - Integer.MAX_VALUE); + Integer.MAX_VALUE, + false); } /** @@ -97,7 +108,7 @@ public static Options defaultOptions() { * @return new options object to use */ public Options locale(Locale locale) { - return new Options(this.graphQLContext, locale, this.maxChildrenDepth); + return new Options(this.graphQLContext, locale, this.maxChildrenDepth, true); } /** @@ -109,7 +120,7 @@ public Options locale(Locale locale) { * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { - return new Options(graphQLContext, this.locale, this.maxChildrenDepth); + return new Options(graphQLContext, this.locale, this.maxChildrenDepth, true); } /** @@ -120,7 +131,18 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { - return new Options(this.graphQLContext, this.locale, maxChildrenDepth); + return new Options(this.graphQLContext, this.locale, maxChildrenDepth, true); + } + + /** + * Controls whether defer execution is supported when creating instances of {@link ExecutableNormalizedOperation}. + * + * @param deferSupport true to enable support for defer + * @return new options object to use + */ + @ExperimentalApi + public Options deferSupport(boolean deferSupport) { + return new Options(this.graphQLContext, this.locale, this.maxChildrenDepth, deferSupport); } /** @@ -146,6 +168,12 @@ public Locale getLocale() { public int getMaxChildrenDepth() { return maxChildrenDepth; } + + // TODO: Javadoc + @ExperimentalApi + public boolean getDeferSupport() { + return deferSupport; + } } private final ConditionalNodes conditionalNodes = new ConditionalNodes(); @@ -166,6 +194,32 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( Document document, String operationName, CoercedVariables coercedVariableValues + ) { + return createExecutableNormalizedOperation( + graphQLSchema, + document, + operationName, + coercedVariableValues, + Options.defaultOptions()); + } + + /** + * This will create a runtime representation of the graphql operation that would be executed + * in a runtime sense. + * + * @param graphQLSchema the schema to be used + * @param document the {@link Document} holding the operation text + * @param operationName the operation name to use + * @param coercedVariableValues the coerced variables to use + * @param options the {@link Options} to use for parsing + * @return a runtime representation of the graphql operation. + */ + public static ExecutableNormalizedOperation createExecutableNormalizedOperation( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + CoercedVariables coercedVariableValues, + Options options ) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, @@ -173,8 +227,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( getOperationResult.fragmentsByName, coercedVariableValues, null, - Options.defaultOptions(), - false); + options); } /** @@ -196,8 +249,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( fragments, coercedVariableValues, null, - Options.defaultOptions(), - false); + Options.defaultOptions()); } /** @@ -273,72 +325,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW getOperationResult.operationDefinition, getOperationResult.fragmentsByName, rawVariables, - options, - false - ); - } - - - /** - * This will create a runtime representation of the graphql operation that would be executed - * in a runtime sense. - *

- * This version of the "createExecutableNormalizedOperation" method has support for the `@defer` directive, - * which is still a experimental feature in GraphQL Java. - * - * @param graphQLSchema the schema to be used - * @param document the {@link Document} holding the operation text - * @param operationName the operation name to use - * @param coercedVariableValues the coerced variables to use - * @return a runtime representation of the graphql operation. - * @see ExecutableNormalizedOperationFactory#createExecutableNormalizedOperation(GraphQLSchema, Document, String, CoercedVariables) - */ - @ExperimentalApi - public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithDeferSupport( - GraphQLSchema graphQLSchema, - Document document, - String operationName, - CoercedVariables coercedVariableValues - ) { - NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, - getOperationResult.operationDefinition, - getOperationResult.fragmentsByName, - coercedVariableValues, - null, - Options.defaultOptions(), - true); - } - - /** - * This will create a runtime representation of the graphql operation that would be executed - * in a runtime sense. - * - *

- * This version of the "createExecutableNormalizedOperationWithRawVariables" method has support for the `@defer` - * directive, which is still a experimental feature in GraphQL Java. - * - * @param graphQLSchema the schema to be used - * @param document the {@link Document} holding the operation text - * @param operationName the operation name to use - * @param rawVariables the raw variables that have not yet been coerced - * @param options the {@link Options} to use for parsing - * @return a runtime representation of the graphql operation. - */ - @ExperimentalApi - public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariablesWithDeferSupport(GraphQLSchema graphQLSchema, - Document document, - String operationName, - RawVariables rawVariables, - Options options) { - NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - - return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, - getOperationResult.operationDefinition, - getOperationResult.fragmentsByName, - rawVariables, - options, - true + options ); } @@ -346,8 +333,7 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit OperationDefinition operationDefinition, Map fragments, RawVariables rawVariables, - Options options, - boolean deferSupport) { + Options options) { List variableDefinitions = operationDefinition.getVariableDefinitions(); CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, @@ -364,8 +350,7 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit fragments, coercedVariableValues, normalizedVariableValues, - options, - deferSupport); + options); } /** @@ -376,8 +361,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr Map fragments, CoercedVariables coercedVariableValues, @Nullable Map normalizedVariableValues, - Options options, - boolean deferSupport) { + Options options) { FieldCollectorNormalizedQueryParams parameters = FieldCollectorNormalizedQueryParams .newParameters() .fragments(fragments) @@ -388,7 +372,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr GraphQLObjectType rootType = SchemaUtil.getOperationRootType(graphQLSchema, operationDefinition); - CollectNFResult collectFromOperationResult = collectFromOperation(parameters, operationDefinition, rootType); + CollectNFResult collectFromOperationResult = collectFromOperation(parameters, operationDefinition, rootType, options.getDeferSupport()); ImmutableListMultimap.Builder fieldToNormalizedField = ImmutableListMultimap.builder(); ImmutableMap.Builder normalizedFieldToMergedField = ImmutableMap.builder(); @@ -402,6 +386,13 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; + ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); + + Consumer captureCollectNFResult = (collectNFResult -> { + normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); + }); + for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); MergedField mergedField = newMergedField(fieldAndAstParents); @@ -420,7 +411,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), - deferSupport); + captureCollectNFResult, + options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); @@ -434,7 +426,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), coordinatesToNormalizedFields.build(), - collectFromOperationResult.normalizedFieldToDeferExecution + normalizedFieldToDeferExecution.build() ); } @@ -447,6 +439,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, + Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -454,6 +447,8 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); + captureCollectNFResult.accept(nextLevel); + for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); @@ -472,6 +467,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, + captureCollectNFResult, deferSupport); } } @@ -565,9 +561,8 @@ private Map> fieldsByResultKey(List public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, OperationDefinition operationDefinition, - GraphQLObjectType rootType) { - - + GraphQLObjectType rootType, + boolean deferSupport) { Set possibleObjects = ImmutableSet.of(rootType); List collectedFields = new ArrayList<>(); collectFromSelectionSet(parameters, operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects, null); @@ -577,11 +572,17 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, false); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } + public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, + OperationDefinition operationDefinition, + GraphQLObjectType rootType) { + return this.collectFromOperation(parameters, operationDefinition, rootType, false); + } + private void createNFs(ImmutableList.Builder nfListBuilder, FieldCollectorNormalizedQueryParams parameters, Map> fieldsByName, @@ -602,6 +603,9 @@ private void createNFs(ImmutableList.Builder nfListBu normalizedFieldToAstFields.put(nf, new FieldAndAstParent(collectedField.field, collectedField.astTypeCondition)); } nfListBuilder.add(nf); + if (deferSupport) { + normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + } } if (commonParentsGroups.size() > 1) { parameters.addPossibleMergers(parent, resultKey); @@ -693,6 +697,14 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set allRelevantObjects = objectTypes.build(); Set deferExecutions = deferExecutionsBuilder.build(); + + Set duplicatedLabels = listDuplicatedLabels(deferExecutions); + + if (!duplicatedLabels.isEmpty()) { + // Query validation should pick this up + Assert.assertShouldNeverHappen("Duplicated @defer labels are not allowed: [%s]", String.join(",", duplicatedLabels)); + } + Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecutions)); @@ -705,6 +717,17 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } + private Set listDuplicatedLabels(Collection deferExecutions) { + return deferExecutions.stream() + .map(DeferExecution::getLabel) + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .collect(toSet()); + } private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams parameters, SelectionSet selectionSet, @@ -766,7 +789,11 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + parameters.getCoercedVariableValues(), + fragmentSpread.getDirectives(), + fragmentDefinition.getTypeCondition() + ); GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); @@ -789,10 +816,13 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter if (inlineFragment.getTypeCondition() != null) { newAstTypeCondition = (GraphQLCompositeType) parameters.getGraphQLSchema().getType(inlineFragment.getTypeCondition().getName()); newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + parameters.getCoercedVariableValues(), + inlineFragment.getDirectives(), + inlineFragment.getTypeCondition() + ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 1737eb9dec..156c31e3d6 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -1,6 +1,7 @@ package graphql.normalized; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import graphql.Assert; import graphql.Directives; @@ -150,7 +151,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @Nullable VariablePredicate variablePredicate, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); } @@ -176,7 +177,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); } @@ -186,7 +187,7 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); @@ -213,7 +214,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { if (normalizedFieldToDeferExecution != null) { return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); } else { @@ -234,13 +235,13 @@ private static List> subselectionsForNormalizedFieldNoDeferSupport( for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null) .forEach((objectTypeName, field) -> fieldsByTypeCondition .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null)); } } @@ -261,7 +262,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - @NotNull Map normalizedFieldToDeferExecution, + @NotNull ImmutableListMultimap normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -271,24 +272,37 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - DeferExecution deferExecution = normalizedFieldToDeferExecution.get(nf); + List deferExecutions = normalizedFieldToDeferExecution.get(nf); if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution) .forEach((objectTypeName, field) -> { - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) - .add(field); + if (deferExecutions.isEmpty()) { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) + .add(field); + } else { + deferExecutions.forEach(deferExecution -> { + if (deferExecution.getTargetType() == null || + objectTypeName.equals(deferExecution.getTargetType().getName())) { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) + .add(field); + } + }); + } }); - } else if (deferExecution != null) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true); + } else if (!deferExecutions.isEmpty()) { + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(null, deferExecution), ignored -> new ArrayList<>()) - .add(field); + deferExecutions.forEach(deferExecution -> { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, deferExecution), ignored -> new ArrayList<>()) + .add(field); + }); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); } } @@ -325,12 +339,11 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map normalizedFieldToDeferExecution, - boolean deferSupport) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, deferSupport)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); } return groupedFields; @@ -344,8 +357,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map normalizedFieldToDeferExecution, - boolean deferSupport) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index f201be211b..8d62c0acaa 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -1,18 +1,36 @@ package graphql.normalized.incremental; import graphql.ExperimentalApi; +import graphql.language.TypeName; import javax.annotation.Nullable; +/** + * Represents the defer execution aspect of a query field + */ @ExperimentalApi public class DeferExecution { private final String label; + private final TypeName targetType; - public DeferExecution(@Nullable String label) { + public DeferExecution(@Nullable String label, @Nullable TypeName targetType) { this.label = label; + this.targetType = targetType; } + /** + * @return the label associated with this defer execution + */ + @Nullable public String getLabel() { return label; } + + /** + * @return the {@link TypeName} of the type that is the target of the defer execution + */ + @Nullable + public TypeName getTargetType() { + return targetType; + } } diff --git a/src/main/java/graphql/normalized/incremental/DeferLabel.java b/src/main/java/graphql/normalized/incremental/DeferLabel.java deleted file mode 100644 index 4c6249bd62..0000000000 --- a/src/main/java/graphql/normalized/incremental/DeferLabel.java +++ /dev/null @@ -1,39 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.ExperimentalApi; - -import javax.annotation.Nullable; -import java.util.Objects; - -/** - * Holds the value of the 'label' argument of a defer directive. - *

- * 'value' can be 'null', as 'label' is not a required argument on the '@defer' directive. - */ -@ExperimentalApi -public class DeferLabel { - private final String value; - - public DeferLabel(@Nullable String value) { - this.value = value; - } - - @Nullable - public String getValue() { - return value; - } - - @Override - public int hashCode() { - return Objects.hashCode(this.value); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof DeferLabel) { - return Objects.equals(this.value, ((DeferLabel) obj).value); - } - - return false; - } -} diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 4d09d5ca75..9898616ddc 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -7,7 +7,9 @@ import graphql.execution.ValuesResolver; import graphql.language.Directive; import graphql.language.NodeUtil; +import graphql.language.TypeName; +import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; @@ -17,40 +19,10 @@ @Internal public class IncrementalNodes { - public DeferLabel getDeferLabel( - Map variables, - List directives - ) { - Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); - - if (deferDirective != null) { - Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); - - Object flag = argumentValues.get("if"); - Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); - - if (!((Boolean) flag)) { - return null; - } - - Object label = argumentValues.get("label"); - - if (label == null) { - return new DeferLabel(null); - } - - Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - - return new DeferLabel((String) label); - - } - - return null; - } - public DeferExecution getDeferExecution( Map variables, - List directives + List directives, + @Nullable TypeName targetType ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -67,12 +39,12 @@ public DeferExecution getDeferExecution( Object label = argumentValues.get("label"); if (label == null) { - return new DeferExecution(null); + return new DeferExecution(null, targetType); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label); + return new DeferExecution((String) label, targetType); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 3d5f809fa1..9c184e5966 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -1,6 +1,6 @@ package graphql.normalized - +import graphql.AssertException import graphql.ExecutionInput import graphql.GraphQL import graphql.TestUtil @@ -233,7 +233,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "multiple fields and a multiple defers with same label"() { + def "multiple fields and a multiple defers with same label are not allowed"() { given: String query = ''' @@ -254,12 +254,11 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Map variables = [:] when: - List printedTree = executeQueryAndPrintTree(query, variables) + executeQueryAndPrintTree(query, variables) then: - printedTree == ['Query.dog', - 'Dog.name defer[name-defer]', - ] + def exception = thrown(AssertException) + exception.message == "Internal error: should never happen: Duplicated @defer labels are not allowed: [name-defer]" } def "nested defers - no label"() { @@ -482,12 +481,12 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, null, RawVariables.of(variables), - ExecutableNormalizedOperationFactory.Options.defaultOptions(), + ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true), ) return printTreeWithIncrementalExecutionDetails(tree) } @@ -495,6 +494,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private List printTreeWithIncrementalExecutionDetails(ExecutableNormalizedOperation queryExecutionTree) { def result = [] Traverser traverser = Traverser.depthFirst({ it.getChildren() }) + + def normalizedFieldToDeferExecution = queryExecutionTree.normalizedFieldToDeferExecution + traverser.traverse(queryExecutionTree.getTopLevelFields(), new TraverserVisitorStub() { @Override TraversalControl enter(TraverserContext context) { @@ -504,12 +506,13 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } String printDeferExecutionDetails(ExecutableNormalizedField field) { - if (field.getDeferExecution() == null) { + def deferExecutions = normalizedFieldToDeferExecution.get(field) + if (deferExecutions.isEmpty()) { return "" } - def deferLabels = field.getDeferExecution().labels - .collect { it.value } + def deferLabels = deferExecutions + .collect { it.label } .join(",") return " defer[" + deferLabels + "]" diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 72fa48b247..432075a363 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -2885,11 +2885,9 @@ fragment personName on Person { CoercedVariables coercedVariableValues ) { ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - if (deferSupport) { - return dependencyGraph.createExecutableNormalizedOperationWithDeferSupport(graphQLSchema, document, operationName, coercedVariableValues) - } else { - return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues) - } + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) + + return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) } private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( @@ -2899,22 +2897,15 @@ fragment personName on Person { RawVariables rawVariables ) { ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - if (deferSupport) { - return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( - graphQLSchema, - document, - operationName, - rawVariables, - ExecutableNormalizedOperationFactory.Options.defaultOptions() - ) - } else { - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( - graphQLSchema, - document, - operationName, - rawVariables - ) - } + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) + + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + operationName, + rawVariables, + options + ) } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index ac7f58b9aa..c334620a17 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -1,5 +1,6 @@ package graphql.normalized + import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables @@ -72,9 +73,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -100,9 +101,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -131,9 +132,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -150,30 +151,33 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } - def "multiple defers with same label on the same field"() { + def "multiple defers without label on the same field"() { String query = """ query q { dog { name - ... @defer(label: "breed-defer") { + ... @defer { breed } - ... @defer(label: "breed-defer") { + ... @defer { breed } } } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ dog { name - ... @defer(label: "breed-defer") { + ... @defer { + breed + } + ... @defer { breed } } @@ -181,29 +185,27 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } - def "multiple defers without label on the same field"() { + def "field with and without defer"() { String query = """ query q { dog { - name ... @defer { breed } - ... @defer { + ... { breed } } } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ dog { - name ... @defer { breed } @@ -231,9 +233,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -268,9 +270,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -278,6 +280,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { name } + ... @defer { + name + } } } ''' @@ -297,9 +302,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -315,6 +320,76 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "2 fragments on conditional fields with different labels"() { + String query = """ + query q { + animal { + ... on Cat @defer(label: "cat-defer") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def tree = createNormalizedTree(schema, query) + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... on Cat @defer(label: "cat-defer") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } +} +''' + } + + def "fragments on conditional fields with different labels and repeating types"() { + String query = """ + query q { + animal { + ... on Cat @defer(label: "cat-defer-1") { + breed + } + ... on Cat @defer(label: "cat-defer-2") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def tree = createNormalizedTree(schema, query) + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... on Cat @defer(label: "cat-defer-1") { + breed + } + ... on Cat @defer(label: "cat-defer-2") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } +} +''' + } + def "nested defer"() { String query = """ query q { @@ -345,19 +420,25 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ animal { + ... @defer { + name + } ... @defer { name } ... on Dog @defer { owner { firstname + ... @defer { + lastname + } ... @defer { bestFriend { firstname @@ -365,7 +446,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification lastname } } - lastname } } } @@ -393,26 +473,26 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ - dog { - ... @defer { - name - } - ... @defer { - breed - } - ... @defer { - owner { - firstname - } - } + dog { + ... @defer { + name + } + ... @defer { + breed + } + ... @defer { + owner { + firstname + } } } +} ''' } @@ -421,19 +501,16 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification Document originalDocument = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true) + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( schema, originalDocument, null, RawVariables.of(variables), - ExecutableNormalizedOperationFactory.Options.defaultOptions() + options ) } - private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { - return createNormalizedTree(schema, query, variables).getTopLevelFields() - } - private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() assert graphQL.execute(newExecutionInput().query(query).variables(variables)).errors.isEmpty() diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index ec88d85dfc..e0720dca3c 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -1,5 +1,6 @@ package graphql.normalized +import com.google.common.collect.ImmutableListMultimap import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables @@ -11,6 +12,7 @@ import graphql.language.Field import graphql.language.IntValue import graphql.language.OperationDefinition import graphql.language.StringValue +import graphql.normalized.incremental.DeferExecution import graphql.parser.Parser import graphql.schema.GraphQLSchema import graphql.schema.idl.RuntimeWiring @@ -201,10 +203,9 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat """ def tree = createNormalizedTree(schema, query) - // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -255,7 +256,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -336,7 +337,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -428,7 +429,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -523,7 +524,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -590,7 +591,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -647,7 +648,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -705,7 +706,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -772,7 +773,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -870,7 +871,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -968,7 +969,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1284,7 +1285,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat when: - def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables, null) OperationDefinition operationDefinition = result.document.getDefinitionsOfType(OperationDefinition.class)[0] def fooField = (Field) operationDefinition.selectionSet.children[0] def nameField = (Field) fooField.selectionSet.children[0] @@ -2147,7 +2148,8 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat Document originalDocument = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables), options) } private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { @@ -2173,7 +2175,18 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat List topLevelFields, VariablePredicate variablePredicate ) { - return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate) + return localCompileToDocument(schema, operationKind, operationName, topLevelFields, variablePredicate, null) + } + + private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( + GraphQLSchema schema, + OperationDefinition.Operation operationKind, + String operationName, + List topLevelFields, + VariablePredicate variablePredicate, + ImmutableListMultimap normalizedFieldToDeferExecution + ) { + return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution) } private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( @@ -2182,9 +2195,11 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat String operationName, List topLevelFields, Map normalizedFieldToQueryDirectives, - VariablePredicate variablePredicate) { + VariablePredicate variablePredicate, + ImmutableListMultimap normalizedFieldToDeferExecution + ) { if (deferSupport) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, [:]) + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution) } return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } From da2761e8355b082046c09c6c1bd1b0d22022d195 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 20 Dec 2023 11:50:31 +1100 Subject: [PATCH 051/393] Clean up defer execution code --- .../java/graphql/DeferredExecutionResult.java | 16 ---- .../graphql/DeferredExecutionResultImpl.java | 68 --------------- .../graphql/execution/defer/DeferSupport.java | 82 ------------------- .../graphql/execution/defer/DeferredCall.java | 43 ---------- .../execution/defer/DeferredErrorSupport.java | 31 ------- .../DeferredFieldInstrumentationContext.java | 12 --- ...nstrumentationDeferredFieldParameters.java | 25 ------ 7 files changed, 277 deletions(-) delete mode 100644 src/main/java/graphql/DeferredExecutionResult.java delete mode 100644 src/main/java/graphql/DeferredExecutionResultImpl.java delete mode 100644 src/main/java/graphql/execution/defer/DeferSupport.java delete mode 100644 src/main/java/graphql/execution/defer/DeferredCall.java delete mode 100644 src/main/java/graphql/execution/defer/DeferredErrorSupport.java delete mode 100644 src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java delete mode 100644 src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java diff --git a/src/main/java/graphql/DeferredExecutionResult.java b/src/main/java/graphql/DeferredExecutionResult.java deleted file mode 100644 index 18eb23c74a..0000000000 --- a/src/main/java/graphql/DeferredExecutionResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package graphql; - -import java.util.List; - -/** - * Results that come back from @defer fields have an extra path property that tells you where - * that deferred result came in the original query - */ -@PublicApi -public interface DeferredExecutionResult extends ExecutionResult { - - /** - * @return the execution path of this deferred result in the original query - */ - List getPath(); -} diff --git a/src/main/java/graphql/DeferredExecutionResultImpl.java b/src/main/java/graphql/DeferredExecutionResultImpl.java deleted file mode 100644 index b5a57eadd4..0000000000 --- a/src/main/java/graphql/DeferredExecutionResultImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package graphql; - -import graphql.execution.ResultPath; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static graphql.Assert.assertNotNull; - -/** - * Results that come back from @defer fields have an extra path property that tells you where - * that deferred result came in the original query - */ -@PublicApi -public class DeferredExecutionResultImpl extends ExecutionResultImpl implements DeferredExecutionResult { - - private final List path; - - private DeferredExecutionResultImpl(List path, ExecutionResultImpl executionResult) { - super(executionResult); - this.path = assertNotNull(path); - } - - /** - * @return the execution path of this deferred result in the original query - */ - public List getPath() { - return path; - } - - @Override - public Map toSpecification() { - Map map = new LinkedHashMap<>(super.toSpecification()); - map.put("path", path); - return map; - } - - public static Builder newDeferredExecutionResult() { - return new Builder(); - } - - public static class Builder { - private List path = Collections.emptyList(); - private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); - - public Builder path(ResultPath path) { - this.path = assertNotNull(path).toList(); - return this; - } - - public Builder from(ExecutionResult executionResult) { - builder.from(executionResult); - return this; - } - - public Builder addErrors(List errors) { - builder.addErrors(errors); - return this; - } - - public DeferredExecutionResult build() { - ExecutionResultImpl build = (ExecutionResultImpl) builder.build(); - return new DeferredExecutionResultImpl(path, build); - } - } -} diff --git a/src/main/java/graphql/execution/defer/DeferSupport.java b/src/main/java/graphql/execution/defer/DeferSupport.java deleted file mode 100644 index 3bc777b8c5..0000000000 --- a/src/main/java/graphql/execution/defer/DeferSupport.java +++ /dev/null @@ -1,82 +0,0 @@ -package graphql.execution.defer; - -import graphql.DeferredExecutionResult; -import graphql.Directives; -import graphql.ExecutionResult; -import graphql.Internal; -import graphql.execution.MergedField; -import graphql.execution.ValuesResolver; -import graphql.execution.reactive.SingleSubscriberPublisher; -import graphql.language.Directive; -import graphql.language.Field; -import org.reactivestreams.Publisher; - -import java.util.Deque; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.atomic.AtomicBoolean; - -import static graphql.Directives.*; - -/** - * This provides support for @defer directives on fields that mean that results will be sent AFTER - * the main result is sent via a Publisher stream. - */ -@Internal -public class DeferSupport { - - private final AtomicBoolean deferDetected = new AtomicBoolean(false); - private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); - private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); -// private final ValuesResolver valuesResolver = new ValuesResolver(); - -// public boolean checkForDeferDirective(MergedField currentField, Map variables) { -// for (Field field : currentField.getFields()) { -// Directive directive = field.getDirective(DeferDirective.getName()); -// if (directive != null) { -// Map argumentValues = valuesResolver.getArgumentValues(DeferDirective.getArguments(), directive.getArguments(), variables); -// return (Boolean) argumentValues.get("if"); -// } -// } -// return false; -// } -// -// @SuppressWarnings("FutureReturnValueIgnored") -// private void drainDeferredCalls() { -// if (deferredCalls.isEmpty()) { -// publisher.noMoreData(); -// return; -// } -// DeferredCall deferredCall = deferredCalls.pop(); -// CompletableFuture future = deferredCall.invoke(); -// future.whenComplete((executionResult, exception) -> { -// if (exception != null) { -// publisher.offerError(exception); -// return; -// } -// publisher.offer(executionResult); -// drainDeferredCalls(); -// }); -// } -// -// public void enqueue(DeferredCall deferredCall) { -// deferDetected.set(true); -// deferredCalls.offer(deferredCall); -// } -// -// public boolean isDeferDetected() { -// return deferDetected.get(); -// } -// -// /** -// * When this is called the deferred execution will begin -// * -// * @return the publisher of deferred results -// */ -// public Publisher startDeferredCalls() { -// drainDeferredCalls(); -// return publisher; -// } -} diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java deleted file mode 100644 index b52e124d5b..0000000000 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ /dev/null @@ -1,43 +0,0 @@ -package graphql.execution.defer; - -import graphql.DeferredExecutionResult; -import graphql.DeferredExecutionResultImpl; -import graphql.ExecutionResult; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.ResultPath; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; - -/** - * This represents a deferred call (aka @defer) to get an execution result sometime after - * the initial query has returned - */ -@Internal -public class DeferredCall { - private final ResultPath path; - private final Supplier> call; - private final DeferredErrorSupport errorSupport; - - public DeferredCall(ResultPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { - this.path = path; - this.call = call; - this.errorSupport = deferredErrorSupport; - } - - CompletableFuture invoke() { - CompletableFuture future = call.get(); - return future.thenApply(this::transformToDeferredResult); - } - - private DeferredExecutionResult transformToDeferredResult(ExecutionResult executionResult) { - List errorsEncountered = errorSupport.getErrors(); - DeferredExecutionResultImpl.Builder builder = DeferredExecutionResultImpl.newDeferredExecutionResult().from(executionResult); - return builder - .addErrors(errorsEncountered) - .path(path) - .build(); - } -} diff --git a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java deleted file mode 100644 index 3ef4f34c5d..0000000000 --- a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java +++ /dev/null @@ -1,31 +0,0 @@ -package graphql.execution.defer; - -import graphql.ExceptionWhileDataFetching; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.ExecutionStrategyParameters; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * This captures errors that occur while a deferred call is being made - */ -@Internal -public class DeferredErrorSupport { - - private final List errors = new CopyOnWriteArrayList<>(); - - public void onFetchingException(ExecutionStrategyParameters parameters, Throwable e) { - ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(parameters.getPath(), e, parameters.getField().getSingleField().getSourceLocation()); - onError(error); - } - - public void onError(GraphQLError gError) { - errors.add(gError); - } - - public List getErrors() { - return errors; - } -} diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java deleted file mode 100644 index 39de62be19..0000000000 --- a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java +++ /dev/null @@ -1,12 +0,0 @@ -package graphql.execution.instrumentation; - -import graphql.ExecutionResult; -import graphql.execution.FieldValueInfo; - -public interface DeferredFieldInstrumentationContext extends InstrumentationContext { - - default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - - } - -} diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java deleted file mode 100644 index e71d104438..0000000000 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java +++ /dev/null @@ -1,25 +0,0 @@ -package graphql.execution.instrumentation.parameters; - -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.ExecutionStrategyParameters; - -import java.util.function.Supplier; - -/** - * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods - */ -public class InstrumentationDeferredFieldParameters extends InstrumentationFieldParameters { - - private final ExecutionStrategyParameters executionStrategyParameters; - - - public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, ExecutionStrategyParameters executionStrategyParameters) { - super(executionContext, executionStepInfo); - this.executionStrategyParameters = executionStrategyParameters; - } - - public ExecutionStrategyParameters getExecutionStrategyParameters() { - return executionStrategyParameters; - } -} From 3f6e480e4ac5eae4fd52ff926fc329b392cee95c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 20 Dec 2023 11:53:01 +1100 Subject: [PATCH 052/393] Add missing Javadoc --- .../normalized/ExecutableNormalizedOperationFactory.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 33682b176b..c70e5de517 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -169,7 +169,10 @@ public int getMaxChildrenDepth() { return maxChildrenDepth; } - // TODO: Javadoc + /** + * @return whether support for defer is enabled + * @see #deferSupport(boolean) + */ @ExperimentalApi public boolean getDeferSupport() { return deferSupport; From 7694618622f06d4a8bee81abe3684c5736838d71 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 21 Dec 2023 09:19:22 +1100 Subject: [PATCH 053/393] WIP: Created execution result classes for incremental delivery --- src/main/java/graphql/defer/DeferredItem.java | 56 +++++++++ .../defer/IncrementalExecutionResult.java | 14 +++ .../defer/IncrementalExecutionResultImpl.java | 60 ++++++++++ .../java/graphql/defer/IncrementalItem.java | 113 ++++++++++++++++++ .../InitialIncrementalExecutionResult.java | 12 ++ ...InitialIncrementalExecutionResultImpl.java | 70 +++++++++++ src/main/java/graphql/defer/StreamedItem.java | 57 +++++++++ 7 files changed, 382 insertions(+) create mode 100644 src/main/java/graphql/defer/DeferredItem.java create mode 100644 src/main/java/graphql/defer/IncrementalExecutionResult.java create mode 100644 src/main/java/graphql/defer/IncrementalExecutionResultImpl.java create mode 100644 src/main/java/graphql/defer/IncrementalItem.java create mode 100644 src/main/java/graphql/defer/InitialIncrementalExecutionResult.java create mode 100644 src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java create mode 100644 src/main/java/graphql/defer/StreamedItem.java diff --git a/src/main/java/graphql/defer/DeferredItem.java b/src/main/java/graphql/defer/DeferredItem.java new file mode 100644 index 0000000000..248977d3c9 --- /dev/null +++ b/src/main/java/graphql/defer/DeferredItem.java @@ -0,0 +1,56 @@ +package graphql.defer; + +import graphql.ExperimentalApi; + +import java.util.LinkedHashMap; +import java.util.Map; + +@ExperimentalApi +public class DeferredItem extends IncrementalItem { + private final Object data; + + private DeferredItem(Object data, IncrementalItem incrementalExecutionResult) { + super(incrementalExecutionResult); + this.data = data; + } + + public T getData() { + //noinspection unchecked + return (T) this.data; + } + + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + + if (data != null) { + map.put("data", data); + } + + return map; + } + + public static DeferredItem.Builder newDeferredItem() { + return new DeferredItem.Builder(); + } + + public static class Builder extends IncrementalItem.Builder { + private Object data = null; + private final IncrementalItem.Builder builder = IncrementalItem.newIncrementalExecutionResult(); + + public Builder data(Object data) { + this.data = data; + return this; + } + + public Builder from(IncrementalItem incrementalExecutionResult) { + builder.from(incrementalExecutionResult); + return this; + } + + public IncrementalItem build() { + IncrementalItem build = builder.build(); + return new DeferredItem(data, build); + } + } +} diff --git a/src/main/java/graphql/defer/IncrementalExecutionResult.java b/src/main/java/graphql/defer/IncrementalExecutionResult.java new file mode 100644 index 0000000000..7590c2b978 --- /dev/null +++ b/src/main/java/graphql/defer/IncrementalExecutionResult.java @@ -0,0 +1,14 @@ +package graphql.defer; + +import graphql.ExperimentalApi; + +import java.util.List; + +@ExperimentalApi +public interface IncrementalExecutionResult { + List getIncremental(); + + String getLabel(); + + boolean hasNext(); +} diff --git a/src/main/java/graphql/defer/IncrementalExecutionResultImpl.java b/src/main/java/graphql/defer/IncrementalExecutionResultImpl.java new file mode 100644 index 0000000000..1b66345a28 --- /dev/null +++ b/src/main/java/graphql/defer/IncrementalExecutionResultImpl.java @@ -0,0 +1,60 @@ +package graphql.defer; + +import java.util.Collections; +import java.util.List; + +public class IncrementalExecutionResultImpl implements IncrementalExecutionResult { + private final List incrementalItems; + private final String label; + private final boolean hasNext; + + private IncrementalExecutionResultImpl(List incrementalItems, String label, boolean hasNext) { + this.incrementalItems = incrementalItems; + this.label = label; + this.hasNext = hasNext; + } + + @Override + public List getIncremental() { + return this.incrementalItems; + } + + @Override + public String getLabel() { + return this.label; + } + + @Override + public boolean hasNext() { + return this.hasNext; + } + + public static Builder newIncrementalExecutionResult() { + return new Builder(); + } + + public static class Builder { + private boolean hasNext = false; + private List incrementalItems = Collections.emptyList(); + private String label = null; + + public IncrementalExecutionResultImpl.Builder hasNext(boolean hasNext) { + this.hasNext = hasNext; + return this; + } + + public IncrementalExecutionResultImpl.Builder incrementalItems(List incrementalItems) { + this.incrementalItems = incrementalItems; + return this; + } + + public IncrementalExecutionResultImpl.Builder label(String label) { + this.label = label; + return this; + } + + public IncrementalExecutionResultImpl build() { + return new IncrementalExecutionResultImpl(this.incrementalItems, this.label, this.hasNext); + } + } +} diff --git a/src/main/java/graphql/defer/IncrementalItem.java b/src/main/java/graphql/defer/IncrementalItem.java new file mode 100644 index 0000000000..9f315916d2 --- /dev/null +++ b/src/main/java/graphql/defer/IncrementalItem.java @@ -0,0 +1,113 @@ +package graphql.defer; + +import graphql.ExperimentalApi; +import graphql.GraphQLError; +import graphql.execution.ResultPath; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toList; + +@ExperimentalApi +public class IncrementalItem { + private final List path; + private final List errors; + private final transient Map extensions; + + private IncrementalItem(List path, List errors, Map extensions) { + this.path = path; + this.errors = errors; + this.extensions = extensions; + } + + IncrementalItem(IncrementalItem incrementalItem) { + this(incrementalItem.getPath(), incrementalItem.getErrors(), incrementalItem.getExtensions()); + } + + public List getPath() { + return null; + } + + public List getErrors() { + return null; + } + + public Map getExtensions() { + return null; + } + + public Map toSpecification() { + Map result = new LinkedHashMap<>(); + if (errors != null && !errors.isEmpty()) { + result.put("errors", errorsToSpec(errors)); + } + if (extensions != null) { + result.put("extensions", extensions); + } + if (path != null) { + result.put("path", path); + } + return result; + } + + private Object errorsToSpec(List errors) { + return errors.stream().map(GraphQLError::toSpecification).collect(toList()); + } + + static IncrementalItem.Builder newIncrementalExecutionResult() { + return new IncrementalItem.Builder(); + } + + public static class Builder { + private List path; + private List errors = new ArrayList<>(); + private Map extensions; + + public IncrementalItem.Builder from(IncrementalItem incrementalExecutionResult) { + path = incrementalExecutionResult.getPath(); + errors = new ArrayList<>(incrementalExecutionResult.getErrors()); + extensions = incrementalExecutionResult.getExtensions(); + return this; + } + + public IncrementalItem.Builder path(ResultPath path) { + if (path != null) { + this.path = path.toList(); + } + return this; + } + + public IncrementalItem.Builder errors(List errors) { + this.errors = errors; + return this; + } + + public IncrementalItem.Builder addErrors(List errors) { + this.errors.addAll(errors); + return this; + } + + public IncrementalItem.Builder addError(GraphQLError error) { + this.errors.add(error); + return this; + } + + public IncrementalItem.Builder extensions(Map extensions) { + this.extensions = extensions; + return this; + } + + public IncrementalItem.Builder addExtension(String key, Object value) { + this.extensions = (this.extensions == null ? new LinkedHashMap<>() : this.extensions); + this.extensions.put(key, value); + return this; + } + + public IncrementalItem build() { + return new IncrementalItem(path, errors, extensions); + } + } +} diff --git a/src/main/java/graphql/defer/InitialIncrementalExecutionResult.java b/src/main/java/graphql/defer/InitialIncrementalExecutionResult.java new file mode 100644 index 0000000000..f38910e9b3 --- /dev/null +++ b/src/main/java/graphql/defer/InitialIncrementalExecutionResult.java @@ -0,0 +1,12 @@ +package graphql.defer; + +import graphql.ExecutionResult; +import graphql.ExperimentalApi; +import org.reactivestreams.Publisher; + +@ExperimentalApi +public interface InitialIncrementalExecutionResult extends ExecutionResult { + boolean hasNext(); + + Publisher getIncrementalItemPublisher(); +} diff --git a/src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java b/src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java new file mode 100644 index 0000000000..e90420c6b3 --- /dev/null +++ b/src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java @@ -0,0 +1,70 @@ +package graphql.defer; + +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import org.reactivestreams.Publisher; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class InitialIncrementalExecutionResultImpl extends ExecutionResultImpl implements InitialIncrementalExecutionResult { + private final boolean hasNext; + private final Publisher incrementalItemPublisher; + + private InitialIncrementalExecutionResultImpl( + boolean hasNext, + Publisher incrementalItemPublisher, + ExecutionResultImpl other + ) { + super(other); + this.hasNext = hasNext; + this.incrementalItemPublisher = incrementalItemPublisher; + } + + @Override + public boolean hasNext() { + return this.hasNext; + } + + @Override + public Publisher getIncrementalItemPublisher() { + return incrementalItemPublisher; + } + + public static Builder newInitialIncrementalExecutionResult() { + return new Builder(); + } + + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + map.put("hasNext", hasNext); + return map; + } + + public static class Builder { + private boolean hasNext = true; + private Publisher incrementalItemPublisher; + private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); + + public Builder hasNext(boolean hasNext) { + this.hasNext = hasNext; + return this; + } + + public Builder incrementalItemPublisher(Publisher incrementalItemPublisher) { + this.incrementalItemPublisher = incrementalItemPublisher; + return this; + } + + public Builder from(ExecutionResult executionResult) { + builder.from(executionResult); + return this; + } + + public InitialIncrementalExecutionResult build() { + ExecutionResultImpl build = (ExecutionResultImpl) builder.build(); + return new InitialIncrementalExecutionResultImpl(this.hasNext, this.incrementalItemPublisher, build); + } + } +} diff --git a/src/main/java/graphql/defer/StreamedItem.java b/src/main/java/graphql/defer/StreamedItem.java new file mode 100644 index 0000000000..e8adfd254a --- /dev/null +++ b/src/main/java/graphql/defer/StreamedItem.java @@ -0,0 +1,57 @@ +package graphql.defer; + +import graphql.ExperimentalApi; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@ExperimentalApi +public class StreamedItem extends IncrementalItem { + private final List items; + + private StreamedItem(List items, IncrementalItem incrementalExecutionResult) { + super(incrementalExecutionResult); + this.items = items; + } + + public List getItems() { + //noinspection unchecked + return (List) this.items; + } + + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + + if (items != null) { + map.put("items", items); + } + + return map; + } + + public static StreamedItem.Builder newStreamedItem() { + return new StreamedItem.Builder(); + } + + public static class Builder extends IncrementalItem.Builder { + private List items = null; + private final IncrementalItem.Builder builder = IncrementalItem.newIncrementalExecutionResult(); + + public Builder items(List items) { + this.items = items; + return this; + } + + public Builder from(IncrementalItem incrementalExecutionResult) { + builder.from(incrementalExecutionResult); + return this; + } + + public IncrementalItem build() { + IncrementalItem build = builder.build(); + return new StreamedItem(items, build); + } + } +} From 7f0a37c1111c6d8fdce6b5ecc2775dd40d34daa7 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 21 Dec 2023 09:28:41 +1100 Subject: [PATCH 054/393] Add Javadocs and one more test --- ...tableNormalizedOperationToAstCompiler.java | 12 +++++---- ...izedOperationToAstCompilerDeferTest.groovy | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 156c31e3d6..6d35061965 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -137,11 +137,12 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * - * @param schema the graphql schema to use - * @param operationKind the kind of operation - * @param operationName the name of the operation to use - * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from - * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work * @return a {@link CompilerResult} object * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, VariablePredicate) */ @@ -167,6 +168,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work * @return a {@link CompilerResult} object * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, Map, VariablePredicate) */ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index c334620a17..d2c88489b6 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -89,6 +89,33 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "@defer directives are not generated when map is null"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def tree = createNormalizedTree(schema, query) + def normalizedFieldToDeferExecution = null + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, normalizedFieldToDeferExecution) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + breed + name + } +} +''' + } + def "simple defer with named spread"() { String query = """ query q { From b78615e9475a03f2d84f83d4a8e9755963019fb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:11:55 +0000 Subject: [PATCH 055/393] Bump com.fasterxml.jackson.core:jackson-databind from 2.16.0 to 2.16.1 Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.16.0 to 2.16.1. - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e474eee399..492d249343 100644 --- a/build.gradle +++ b/build.gradle @@ -112,7 +112,7 @@ dependencies { testImplementation 'org.codehaus.groovy:groovy-json:3.0.19' testImplementation 'com.google.code.gson:gson:2.10.1' testImplementation 'org.eclipse.jetty:jetty-server:11.0.15' - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0' + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' testImplementation 'org.slf4j:slf4j-simple:' + slf4jVersion testImplementation 'org.awaitility:awaitility-groovy:4.2.0' testImplementation 'com.github.javafaker:javafaker:1.0.2' From fed822e4fcd6e73d0bfa8b39ae7f39cf19a1159a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:11:59 +0000 Subject: [PATCH 056/393] Bump org.codehaus.groovy:groovy from 3.0.19 to 3.0.20 Bumps [org.codehaus.groovy:groovy](https://github.com/apache/groovy) from 3.0.19 to 3.0.20. - [Commits](https://github.com/apache/groovy/commits) --- updated-dependencies: - dependency-name: org.codehaus.groovy:groovy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e474eee399..6a47d2c351 100644 --- a/build.gradle +++ b/build.gradle @@ -108,8 +108,8 @@ dependencies { implementation 'com.google.guava:guava:' + guavaVersion testImplementation group: 'junit', name: 'junit', version: '4.13.2' testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0' - testImplementation 'org.codehaus.groovy:groovy:3.0.19' - testImplementation 'org.codehaus.groovy:groovy-json:3.0.19' + testImplementation 'org.codehaus.groovy:groovy:3.0.20' + testImplementation 'org.codehaus.groovy:groovy-json:3.0.20' testImplementation 'com.google.code.gson:gson:2.10.1' testImplementation 'org.eclipse.jetty:jetty-server:11.0.15' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0' From 170b4c81a2eed770c2f192953ae7966cdd6a84c5 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 27 Dec 2023 12:49:31 +1100 Subject: [PATCH 057/393] This removes SLF4J from the code base --- build.gradle | 5 +-- src/main/java/graphql/GraphQL.java | 37 +------------------ .../MaxQueryComplexityInstrumentation.java | 7 ---- .../java/graphql/execution/Execution.java | 7 ---- .../graphql/execution/ExecutionStrategy.java | 19 ---------- .../SimpleDataFetcherExceptionHandler.java | 5 --- .../DataLoaderDispatcherInstrumentation.java | 9 +---- ...aLoaderDispatcherInstrumentationState.java | 5 +-- .../FieldLevelTrackingApproach.java | 8 +--- .../graphql/schema/PropertyFetchingImpl.java | 4 -- .../idl/ArgValueOfAllowedTypeChecker.java | 7 ---- src/main/java/graphql/util/LogKit.java | 22 ----------- .../groovy/graphql/util/LogKitTest.groovy | 14 ------- 13 files changed, 6 insertions(+), 143 deletions(-) delete mode 100644 src/main/java/graphql/util/LogKit.java delete mode 100644 src/test/groovy/graphql/util/LogKitTest.groovy diff --git a/build.gradle b/build.gradle index e474eee399..8c17b0a887 100644 --- a/build.gradle +++ b/build.gradle @@ -61,9 +61,8 @@ def getDevelopmentVersion() { return makeDevelopmentVersion(["0.0.0", branchName, "SNAPSHOT"]) } -def reactiveStreamsVersion = '1.0.3' -def slf4jVersion = '2.0.7' def releaseVersion = System.env.RELEASE_VERSION +def reactiveStreamsVersion = '1.0.3' def antlrVersion = '4.11.1' // https://mvnrepository.com/artifact/org.antlr/antlr4-runtime def guavaVersion = '32.1.2-jre' version = releaseVersion ? releaseVersion : getDevelopmentVersion() @@ -101,7 +100,6 @@ jar { dependencies { compileOnly 'org.jetbrains:annotations:24.1.0' implementation 'org.antlr:antlr4-runtime:' + antlrVersion - implementation 'org.slf4j:slf4j-api:' + slf4jVersion api 'com.graphql-java:java-dataloader:3.2.2' api 'org.reactivestreams:reactive-streams:' + reactiveStreamsVersion antlr 'org.antlr:antlr4:' + antlrVersion @@ -113,7 +111,6 @@ dependencies { testImplementation 'com.google.code.gson:gson:2.10.1' testImplementation 'org.eclipse.jetty:jetty-server:11.0.15' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0' - testImplementation 'org.slf4j:slf4j-simple:' + slf4jVersion testImplementation 'org.awaitility:awaitility-groovy:4.2.0' testImplementation 'com.github.javafaker:javafaker:1.0.2' diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 2ccb54b955..535accfdbe 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -28,10 +28,7 @@ import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.language.Document; import graphql.schema.GraphQLSchema; -import graphql.util.LogKit; import graphql.validation.ValidationError; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; @@ -91,9 +88,6 @@ @PublicApi public class GraphQL { - private static final Logger log = LoggerFactory.getLogger(GraphQL.class); - private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(GraphQL.class); - private final GraphQLSchema graphQLSchema; private final ExecutionStrategy queryStrategy; private final ExecutionStrategy mutationStrategy; @@ -419,9 +413,6 @@ public CompletableFuture executeAsync(UnaryOperator executeAsync(ExecutionInput executionInput) { - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Executing request. operation name: '{}'. query: '{}'. variables '{}'", executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables()); - } ExecutionInput executionInputWithId = ensureInputHasId(executionInput); CompletableFuture instrumentationStateCF = instrumentation.createStateAsync(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInput)); @@ -496,12 +487,8 @@ private PreparsedDocumentEntry parseAndValidate(AtomicReference ExecutionInput executionInput = executionInputRef.get(); String query = executionInput.getQuery(); - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Parsing query: '{}'...", query); - } ParseAndValidateResult parseResult = parse(executionInput, graphQLSchema, instrumentationState); if (parseResult.isFailure()) { - logNotSafe.warn("Query did not parse : '{}'", executionInput.getQuery()); return new PreparsedDocumentEntry(parseResult.getSyntaxException().toInvalidSyntaxError()); } else { final Document document = parseResult.getDocument(); @@ -509,12 +496,8 @@ private PreparsedDocumentEntry parseAndValidate(AtomicReference executionInput = executionInput.transform(builder -> builder.variables(parseResult.getVariables())); executionInputRef.set(executionInput); - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Validating query: '{}'", query); - } final List errors = validate(executionInput, document, graphQLSchema, instrumentationState); if (!errors.isEmpty()) { - logNotSafe.warn("Query did not validate : '{}'", query); return new PreparsedDocumentEntry(document, errors); } @@ -562,25 +545,7 @@ private CompletableFuture execute(ExecutionInput executionInput Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer); ExecutionId executionId = executionInput.getExecutionId(); - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Executing '{}'. operation name: '{}'. query: '{}'. variables '{}'", executionId, executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables()); - } - CompletableFuture future = execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState); - future = future.whenComplete((result, throwable) -> { - if (throwable != null) { - logNotSafe.error(String.format("Execution '%s' threw exception when executing : query : '%s'. variables '%s'", executionId, executionInput.getQuery(), executionInput.getVariables()), throwable); - } else { - if (log.isDebugEnabled()) { - 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; + return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState); } private static Instrumentation checkInstrumentationDefaultState(Instrumentation instrumentation, boolean doNotAddDefaultInstrumentations) { diff --git a/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java b/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java index 87a00e976a..b952214948 100644 --- a/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java +++ b/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java @@ -12,8 +12,6 @@ import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.validation.ValidationError; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -32,8 +30,6 @@ @PublicApi public class MaxQueryComplexityInstrumentation extends SimplePerformantInstrumentation { - private static final Logger log = LoggerFactory.getLogger(MaxQueryComplexityInstrumentation.class); - private final int maxComplexity; private final FieldComplexityCalculator fieldComplexityCalculator; private final Function maxQueryComplexityExceededFunction; @@ -100,9 +96,6 @@ public InstrumentationState createState(InstrumentationCreateStateParameters par State state = ofState(rawState); QueryComplexityCalculator queryComplexityCalculator = newQueryComplexityCalculator(instrumentationExecuteOperationParameters.getExecutionContext()); int totalComplexity = queryComplexityCalculator.calculate(); - if (log.isDebugEnabled()) { - log.debug("Query complexity: {}", totalComplexity); - } if (totalComplexity > maxComplexity) { QueryComplexityInfo queryComplexityInfo = QueryComplexityInfo.newQueryComplexityInfo() .complexity(totalComplexity) diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 916bc64659..133ef2cda9 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -21,8 +21,6 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.impl.SchemaUtil; -import graphql.util.LogKit; -import org.slf4j.Logger; import java.util.Collections; import java.util.List; @@ -37,8 +35,6 @@ @Internal public class Execution { - private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(Execution.class); - private final FieldCollector fieldCollector = new FieldCollector(); private final ExecutionStrategy queryStrategy; private final ExecutionStrategy mutationStrategy; @@ -155,9 +151,6 @@ private CompletableFuture executeOperation(ExecutionContext exe CompletableFuture result; try { ExecutionStrategy executionStrategy = executionContext.getStrategy(operation); - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Executing '{}' query operation: '{}' using '{}' execution strategy", executionContext.getExecutionId(), operation, executionStrategy.getClass().getName()); - } result = executionStrategy.execute(executionContext, parameters); } catch (NonNullableFieldWasNullException e) { // this means it was non-null types all the way from an offending non-null type diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 4ed0f1e644..fd22112b45 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -40,9 +40,6 @@ import graphql.schema.GraphQLType; import graphql.schema.LightDataFetcher; import graphql.util.FpKit; -import graphql.util.LogKit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; @@ -126,9 +123,6 @@ @SuppressWarnings("FutureReturnValueIgnored") public abstract class ExecutionStrategy { - private static final Logger log = LoggerFactory.getLogger(ExecutionStrategy.class); - private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(ExecutionStrategy.class); - protected final FieldCollector fieldCollector = new FieldCollector(); protected final ExecutionStepInfoFactory executionStepInfoFactory = new ExecutionStepInfoFactory(); private final ResolveType resolvedType = new ResolveType(); @@ -312,9 +306,6 @@ private CompletableFuture invokeDataFetcher(ExecutionContext executionCo } fetchedValue = Async.toCompletableFuture(fetchedValueRaw); } catch (Exception e) { - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug(String.format("'%s', field '%s' fetch threw exception", executionContext.getExecutionId(), parameters.getPath()), e); - } fetchedValue = Async.exceptionallyCompletedFuture(e); } return fetchedValue; @@ -428,10 +419,6 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut .nonNullFieldValidator(nonNullableFieldValidator) ); - if (log.isDebugEnabled()) { - log.debug("'{}' completing field '{}'...", executionContext.getExecutionId(), executionStepInfo.getPath()); - } - FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters); CompletableFuture executionResultFuture = fieldValueInfo.getFieldValue(); @@ -493,9 +480,7 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStrategyParameters parameters, UnresolvedTypeException e) { UnresolvedTypeError error = new UnresolvedTypeError(parameters.getPath(), parameters.getExecutionStepInfo(), e); - logNotSafe.warn(error.getMessage(), e); context.addError(error); - } /** @@ -705,10 +690,7 @@ protected CompletableFuture completeValueForObject(ExecutionCon @SuppressWarnings("SameReturnValue") private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategyParameters parameters, CoercingSerializeException e) { SerializationError error = new SerializationError(parameters.getPath(), e); - logNotSafe.warn(error.getMessage(), e); context.addError(error); - - return null; } @@ -733,7 +715,6 @@ protected Iterable toIterable(ExecutionContext context, ExecutionStrateg private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) { TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getExecutionStepInfo().getUnwrappedNonNullType()); - logNotSafe.warn("{} got {}", error.getMessage(), result.getClass()); context.addError(error); } diff --git a/src/main/java/graphql/execution/SimpleDataFetcherExceptionHandler.java b/src/main/java/graphql/execution/SimpleDataFetcherExceptionHandler.java index 606de0f8a9..79e201a333 100644 --- a/src/main/java/graphql/execution/SimpleDataFetcherExceptionHandler.java +++ b/src/main/java/graphql/execution/SimpleDataFetcherExceptionHandler.java @@ -3,8 +3,6 @@ import graphql.ExceptionWhileDataFetching; import graphql.PublicApi; import graphql.language.SourceLocation; -import graphql.util.LogKit; -import org.slf4j.Logger; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -16,8 +14,6 @@ @PublicApi public class SimpleDataFetcherExceptionHandler implements DataFetcherExceptionHandler { - private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(SimpleDataFetcherExceptionHandler.class); - static final SimpleDataFetcherExceptionHandler defaultImpl = new SimpleDataFetcherExceptionHandler(); private DataFetcherExceptionHandlerResult handleExceptionImpl(DataFetcherExceptionHandlerParameters handlerParameters) { @@ -43,7 +39,6 @@ public CompletableFuture handleException(Data * @param exception the exception that happened */ protected void logException(ExceptionWhileDataFetching error, Throwable exception) { - logNotSafe.warn(error.getMessage(), exception); } /** diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 87c137e303..8fc8accfde 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -49,9 +49,6 @@ */ @PublicApi public class DataLoaderDispatcherInstrumentation extends SimplePerformantInstrumentation { - - private static final Logger log = LoggerFactory.getLogger(DataLoaderDispatcherInstrumentation.class); - private final DataLoaderDispatcherInstrumentationOptions options; /** @@ -73,7 +70,7 @@ public DataLoaderDispatcherInstrumentation(DataLoaderDispatcherInstrumentationOp @Override public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return new DataLoaderDispatcherInstrumentationState(log, parameters.getExecutionInput().getDataLoaderRegistry()); + return new DataLoaderDispatcherInstrumentationState(parameters.getExecutionInput().getDataLoaderRegistry()); } @Override @@ -158,10 +155,6 @@ private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContex Map dataLoaderStats = buildStatsMap(state); statsMap.put("dataloader", dataLoaderStats); - if (log.isDebugEnabled()) { - log.debug("Data loader stats : {}", dataLoaderStats); - } - return CompletableFuture.completedFuture(new ExecutionResultImpl(executionResult.getData(), executionResult.getErrors(), statsMap)); } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java index 1d6a697fa1..31d1db0499 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java @@ -6,7 +6,6 @@ import graphql.execution.instrumentation.InstrumentationState; import org.dataloader.DataLoader; import org.dataloader.DataLoaderRegistry; -import org.slf4j.Logger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -45,9 +44,9 @@ public DataLoaderRegistry unregister(String key) { private volatile boolean aggressivelyBatching = true; private volatile boolean hasNoDataLoaders; - public DataLoaderDispatcherInstrumentationState(Logger log, DataLoaderRegistry dataLoaderRegistry) { + public DataLoaderDispatcherInstrumentationState(DataLoaderRegistry dataLoaderRegistry) { this.dataLoaderRegistry = new AtomicReference<>(dataLoaderRegistry); - this.approach = new FieldLevelTrackingApproach(log, this::getDataLoaderRegistry); + this.approach = new FieldLevelTrackingApproach(this::getDataLoaderRegistry); this.state = approach.createState(); hasNoDataLoaders = checkForNoDataLoader(dataLoaderRegistry); } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 7da689db9a..a98fac3ae0 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -12,7 +12,6 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.util.LockKit; import org.dataloader.DataLoaderRegistry; -import org.slf4j.Logger; import java.util.LinkedHashSet; import java.util.List; @@ -26,7 +25,6 @@ @Internal public class FieldLevelTrackingApproach { private final Supplier dataLoaderRegistrySupplier; - private final Logger log; private static class CallStack implements InstrumentationState { @@ -112,9 +110,8 @@ public void clearAndMarkCurrentLevelAsReady(int level) { } } - public FieldLevelTrackingApproach(Logger log, Supplier dataLoaderRegistrySupplier) { + public FieldLevelTrackingApproach(Supplier dataLoaderRegistrySupplier) { this.dataLoaderRegistrySupplier = dataLoaderRegistrySupplier; - this.log = log; } public InstrumentationState createState() { @@ -236,9 +233,6 @@ private boolean levelReady(CallStack callStack, int level) { void dispatch() { DataLoaderRegistry dataLoaderRegistry = getDataLoaderRegistry(); - if (log.isDebugEnabled()) { - log.debug("Dispatching data loaders ({})", dataLoaderRegistry.getKeys()); - } dataLoaderRegistry.dispatchAll(); } diff --git a/src/main/java/graphql/schema/PropertyFetchingImpl.java b/src/main/java/graphql/schema/PropertyFetchingImpl.java index da8b153e3d..c5f3b95656 100644 --- a/src/main/java/graphql/schema/PropertyFetchingImpl.java +++ b/src/main/java/graphql/schema/PropertyFetchingImpl.java @@ -32,8 +32,6 @@ */ @Internal public class PropertyFetchingImpl { - private static final Logger log = LoggerFactory.getLogger(PropertyFetchingImpl.class); - private final AtomicBoolean USE_SET_ACCESSIBLE = new AtomicBoolean(true); private final AtomicBoolean USE_LAMBDA_FACTORY = new AtomicBoolean(true); private final AtomicBoolean USE_NEGATIVE_CACHE = new AtomicBoolean(true); @@ -123,8 +121,6 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g // are preventing the Meta Lambda from working. So let's continue with // old skool reflection and if it's all broken there then it will eventually // end up negatively cached - log.debug("Unable to invoke fast Meta Lambda for `{}` - Falling back to reflection", object.getClass().getName(), ignored); - } } diff --git a/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java b/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java index c9d2498abd..9dcd16b768 100644 --- a/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java +++ b/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java @@ -30,8 +30,6 @@ import graphql.schema.CoercingParseLiteralException; import graphql.schema.GraphQLScalarType; import graphql.schema.idl.errors.DirectiveIllegalArgumentTypeError; -import graphql.util.LogKit; -import org.slf4j.Logger; import java.util.List; import java.util.Locale; @@ -64,8 +62,6 @@ @Internal class ArgValueOfAllowedTypeChecker { - private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(ArgValueOfAllowedTypeChecker.class); - private final Directive directive; private final Node element; private final String elementName; @@ -291,9 +287,6 @@ private boolean isArgumentValueScalarLiteral(GraphQLScalarType scalarType, Value scalarType.getCoercing().parseLiteral(instanceValue, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()); return true; } catch (CoercingParseLiteralException ex) { - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Attempted parsing literal into '{}' but got the following error: ", scalarType.getName(), ex); - } return false; } } diff --git a/src/main/java/graphql/util/LogKit.java b/src/main/java/graphql/util/LogKit.java deleted file mode 100644 index df65060e6b..0000000000 --- a/src/main/java/graphql/util/LogKit.java +++ /dev/null @@ -1,22 +0,0 @@ -package graphql.util; - -import graphql.Internal; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Internal -public class LogKit { - - /** - * Creates a logger with a name indicating that the content might not be privacy safe - * eg it could contain user generated content or privacy information. - * - * @param clazz the class to make a logger for - * - * @return a new Logger - */ - public static Logger getNotPrivacySafeLogger(Class clazz) { - return LoggerFactory.getLogger(String.format("notprivacysafe.%s", clazz.getName())); - } - -} diff --git a/src/test/groovy/graphql/util/LogKitTest.groovy b/src/test/groovy/graphql/util/LogKitTest.groovy deleted file mode 100644 index eeb1582baa..0000000000 --- a/src/test/groovy/graphql/util/LogKitTest.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package graphql.util - - -import spock.lang.Specification - -class LogKitTest extends Specification { - - def "logger has a prefixed name"() { - when: - def logger = LogKit.getNotPrivacySafeLogger(LogKitTest.class) - then: - logger.getName() == "notprivacysafe.graphql.util.LogKitTest" - } -} From 698f43348ccea86ac5907e7597641818c4c56c1e Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 31 Dec 2023 14:41:02 +0900 Subject: [PATCH 058/393] Delete deprecated constructors, a builder already exists --- .../graphql/validation/ValidationError.java | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/src/main/java/graphql/validation/ValidationError.java b/src/main/java/graphql/validation/ValidationError.java index 841db1c17a..b3b27d8477 100644 --- a/src/main/java/graphql/validation/ValidationError.java +++ b/src/main/java/graphql/validation/ValidationError.java @@ -24,51 +24,6 @@ public class ValidationError implements GraphQLError { private final List queryPath = new ArrayList<>(); private final ImmutableMap extensions; - @Deprecated - @DeprecatedAt("2022-07-10") - public ValidationError(ValidationErrorClassification validationErrorType) { - this(newValidationError() - .validationErrorType(validationErrorType)); - } - - @Deprecated - @DeprecatedAt("2022-07-10") - public ValidationError(ValidationErrorClassification validationErrorType, SourceLocation sourceLocation, String description) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocation(sourceLocation) - .description(description)); - } - - @Deprecated - @DeprecatedAt("2022-07-10") - public ValidationError(ValidationErrorType validationErrorType, SourceLocation sourceLocation, String description, List queryPath) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocation(sourceLocation) - .description(description) - .queryPath(queryPath)); - } - - @Deprecated - @DeprecatedAt("2022-07-10") - public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocations(sourceLocations) - .description(description)); - } - - @Deprecated - @DeprecatedAt("2022-07-10") - public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description, List queryPath) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocations(sourceLocations) - .description(description) - .queryPath(queryPath)); - } - private ValidationError(Builder builder) { this.validationErrorType = builder.validationErrorType; this.description = builder.description; From 03c4f39594dd0efa810fd053d3d819625db5764e Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 31 Dec 2023 15:26:25 +0900 Subject: [PATCH 059/393] Replace homebrew @DeprecatedAt annotation with Java 9 equivalent --- src/main/java/graphql/DeprecatedAt.java | 22 -------- src/main/java/graphql/DirectivesUtil.java | 23 +++----- src/main/java/graphql/ExecutionInput.java | 12 ++-- .../graphql/TypeResolutionEnvironment.java | 3 +- .../collect/ImmutableMapWithNullValues.java | 40 +++++-------- .../DataFetcherExceptionHandler.java | 1 - .../graphql/execution/DataFetcherResult.java | 4 +- .../graphql/execution/ExecutionContext.java | 7 +-- .../execution/ExecutionContextBuilder.java | 7 +-- .../graphql/execution/ExecutionStepInfo.java | 4 +- .../execution/TypeResolutionParameters.java | 7 +-- .../execution/directives/QueryDirectives.java | 10 +--- .../instrumentation/Instrumentation.java | 56 ++++++------------- .../SimpleInstrumentation.java | 4 +- ...rumentationExecuteOperationParameters.java | 7 +-- .../InstrumentationExecutionParameters.java | 10 +--- ...umentationExecutionStrategyParameters.java | 7 +-- ...nstrumentationFieldCompleteParameters.java | 10 +--- .../InstrumentationFieldFetchParameters.java | 4 +- .../InstrumentationFieldParameters.java | 7 +-- .../InstrumentationValidationParameters.java | 4 +- .../preparsed/PreparsedDocumentProvider.java | 4 +- .../persisted/PersistedQueryCache.java | 4 +- src/main/java/graphql/parser/Parser.java | 10 +--- src/main/java/graphql/schema/Coercing.java | 16 ++---- .../schema/DataFetchingEnvironment.java | 7 +-- .../schema/DataFetchingEnvironmentImpl.java | 4 +- .../DelegatingDataFetchingEnvironment.java | 7 +-- .../java/graphql/schema/GraphQLArgument.java | 22 +++----- .../graphql/schema/GraphQLCodeRegistry.java | 11 +--- .../schema/GraphQLDirectiveContainer.java | 19 ++----- .../schema/GraphQLFieldDefinition.java | 16 ++---- .../schema/GraphQLInputObjectField.java | 4 +- .../graphql/schema/GraphQLInterfaceType.java | 7 +-- .../java/graphql/schema/GraphQLSchema.java | 28 +++------- .../java/graphql/schema/GraphQLUnionType.java | 8 +-- ...GraphqlDirectivesContainerTypeBuilder.java | 13 ++--- .../java/graphql/schema/diff/DiffSet.java | 4 +- .../java/graphql/schema/diff/SchemaDiff.java | 4 +- .../graphql/schema/idl/RuntimeWiring.java | 4 +- .../idl/SchemaDirectiveWiringEnvironment.java | 10 +--- .../idl/SchemaGeneratorPostProcessing.java | 4 +- .../graphql/validation/ValidationError.java | 1 - 43 files changed, 130 insertions(+), 326 deletions(-) delete mode 100644 src/main/java/graphql/DeprecatedAt.java diff --git a/src/main/java/graphql/DeprecatedAt.java b/src/main/java/graphql/DeprecatedAt.java deleted file mode 100644 index 0918e5f6d6..0000000000 --- a/src/main/java/graphql/DeprecatedAt.java +++ /dev/null @@ -1,22 +0,0 @@ -package graphql; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.CONSTRUCTOR; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PACKAGE; -import static java.lang.annotation.ElementType.TYPE; - -/** - * Helper to track deprecation - * - * Please use ISO-8601 format i.e. YYYY-MM-DD - */ -@Retention(RetentionPolicy.SOURCE) -@Target(value = {CONSTRUCTOR, METHOD, TYPE, FIELD, PACKAGE}) -public @interface DeprecatedAt { - String value(); -} diff --git a/src/main/java/graphql/DirectivesUtil.java b/src/main/java/graphql/DirectivesUtil.java index 9a3e1fc3ac..0a560f7233 100644 --- a/src/main/java/graphql/DirectivesUtil.java +++ b/src/main/java/graphql/DirectivesUtil.java @@ -23,9 +23,7 @@ @Internal public class DirectivesUtil { - - @Deprecated // use GraphQLAppliedDirectives eventually - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") // use GraphQLAppliedDirectives eventually public static Map nonRepeatableDirectivesByName(List directives) { // filter the repeatable directives List singletonDirectives = directives.stream() @@ -34,15 +32,13 @@ public static Map nonRepeatableDirectivesByName(List> allDirectivesByName(List directives) { return ImmutableMap.copyOf(FpKit.groupingBy(directives, GraphQLDirective::getName)); } - @Deprecated // use GraphQLAppliedDirectives eventually - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") // use GraphQLAppliedDirectives eventually public static Optional directiveWithArg(List directives, String directiveName, String argumentName) { GraphQLDirective directive = nonRepeatableDirectivesByName(directives).get(directiveName); GraphQLArgument argument = null; @@ -52,9 +48,7 @@ public static Optional directiveWithArg(List return Optional.ofNullable(argument); } - - @Deprecated // use GraphQLAppliedDirectives eventually - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") // use GraphQLAppliedDirectives eventually public static boolean isAllNonRepeatable(List directives) { if (directives == null || directives.isEmpty()) { return false; @@ -67,8 +61,7 @@ public static boolean isAllNonRepeatable(List directives) { return true; } - @Deprecated // use GraphQLAppliedDirectives eventually - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") // use GraphQLAppliedDirectives eventually public static List add(List targetList, GraphQLDirective newDirective) { assertNotNull(targetList, () -> "directive list can't be null"); assertNotNull(newDirective, () -> "directive can't be null"); @@ -76,8 +69,7 @@ public static List add(List targetList, Grap return targetList; } - @Deprecated // use GraphQLAppliedDirectives eventually - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") // use GraphQLAppliedDirectives eventually public static List addAll(List targetList, List newDirectives) { assertNotNull(targetList, () -> "directive list can't be null"); assertNotNull(newDirectives, () -> "directive list can't be null"); @@ -85,8 +77,7 @@ public static List addAll(List targetList, L return targetList; } - @Deprecated // use GraphQLAppliedDirectives eventually - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") // use GraphQLAppliedDirectives eventually public static GraphQLDirective getFirstDirective(String name, Map> allDirectivesByName) { List directives = allDirectivesByName.getOrDefault(name, emptyList()); if (directives.isEmpty()) { diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index f924aab7e4..bfda46a3d3 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -68,8 +68,7 @@ public String getOperationName() { * * @deprecated - use {@link #getGraphQLContext()} */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public Object getContext() { return context; } @@ -273,8 +272,7 @@ public Builder localContext(Object localContext) { * * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public Builder context(Object context) { this.context = context; return this; @@ -289,8 +287,7 @@ public Builder context(Object context) { * * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public Builder context(GraphQLContext.Builder contextBuilder) { this.context = contextBuilder.build(); return this; @@ -305,8 +302,7 @@ public Builder context(GraphQLContext.Builder contextBuilder) { * * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public Builder context(UnaryOperator contextBuilderFunction) { GraphQLContext.Builder builder = GraphQLContext.newContext(); builder = contextBuilderFunction.apply(builder); diff --git a/src/main/java/graphql/TypeResolutionEnvironment.java b/src/main/java/graphql/TypeResolutionEnvironment.java index c606fdd5fe..ca57979bf5 100644 --- a/src/main/java/graphql/TypeResolutionEnvironment.java +++ b/src/main/java/graphql/TypeResolutionEnvironment.java @@ -94,8 +94,7 @@ public GraphQLSchema getSchema() { * * @deprecated use {@link #getGraphQLContext()} instead */ - @Deprecated - @DeprecatedAt("2021-12-27") + @Deprecated(since = "2021-12-27") public T getContext() { //noinspection unchecked return (T) context; diff --git a/src/main/java/graphql/collect/ImmutableMapWithNullValues.java b/src/main/java/graphql/collect/ImmutableMapWithNullValues.java index 8eab40c56d..e9ca664efb 100644 --- a/src/main/java/graphql/collect/ImmutableMapWithNullValues.java +++ b/src/main/java/graphql/collect/ImmutableMapWithNullValues.java @@ -1,7 +1,6 @@ package graphql.collect; import graphql.Assert; -import graphql.DeprecatedAt; import graphql.Internal; import java.util.Collection; @@ -82,29 +81,25 @@ public V get(Object key) { } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V put(K key, V value) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V remove(Object key) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public void putAll(Map m) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public void clear() { throw new UnsupportedOperationException(); } @@ -145,64 +140,55 @@ public void forEach(BiConsumer action) { } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public void replaceAll(BiFunction function) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V putIfAbsent(K key, V value) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public boolean remove(Object key, Object value) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public boolean replace(K key, V oldValue, V newValue) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V replace(K key, V value) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V computeIfAbsent(K key, Function mappingFunction) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V computeIfPresent(K key, BiFunction remappingFunction) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V compute(K key, BiFunction remappingFunction) { throw new UnsupportedOperationException(); } @Override - @Deprecated - @DeprecatedAt("2020-11-10") + @Deprecated(since = "2020-11-10") public V merge(K key, V value, BiFunction remappingFunction) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/graphql/execution/DataFetcherExceptionHandler.java b/src/main/java/graphql/execution/DataFetcherExceptionHandler.java index 7b7d294a0b..6daafd94dd 100644 --- a/src/main/java/graphql/execution/DataFetcherExceptionHandler.java +++ b/src/main/java/graphql/execution/DataFetcherExceptionHandler.java @@ -1,6 +1,5 @@ package graphql.execution; -import graphql.DeprecatedAt; import graphql.ExecutionResult; import graphql.PublicSpi; import graphql.schema.DataFetcher; diff --git a/src/main/java/graphql/execution/DataFetcherResult.java b/src/main/java/graphql/execution/DataFetcherResult.java index 460b07daf9..3baa4f4fec 100644 --- a/src/main/java/graphql/execution/DataFetcherResult.java +++ b/src/main/java/graphql/execution/DataFetcherResult.java @@ -1,7 +1,6 @@ package graphql.execution; import com.google.common.collect.ImmutableList; -import graphql.DeprecatedAt; import graphql.ExecutionResult; import graphql.GraphQLError; import graphql.Internal; @@ -49,8 +48,7 @@ public class DataFetcherResult { * @deprecated use the {@link #newResult()} builder instead */ @Internal - @Deprecated - @DeprecatedAt("2019-01-11") + @Deprecated(since = "2019-01-11") public DataFetcherResult(T data, List errors) { this(data, errors, null, null); } diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 50db89b566..747f955d1a 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -3,7 +3,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.GraphQLError; @@ -120,8 +119,7 @@ public OperationDefinition getOperationDefinition() { * * @deprecated use {@link #getCoercedVariables()} instead */ - @Deprecated - @DeprecatedAt("2022-05-24") + @Deprecated(since = "2022-05-24") public Map getVariables() { return coercedVariables.toMap(); } @@ -137,8 +135,7 @@ public CoercedVariables getCoercedVariables() { * * @deprecated use {@link #getGraphQLContext()} instead */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public T getContext() { return (T) context; diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index f941be07b7..7220e8dee7 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.GraphQLError; @@ -131,8 +130,7 @@ public ExecutionContextBuilder subscriptionStrategy(ExecutionStrategy subscripti /* * @deprecated use {@link #graphQLContext(GraphQLContext)} instead */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public ExecutionContextBuilder context(Object context) { this.context = context; return this; @@ -159,8 +157,7 @@ public ExecutionContextBuilder root(Object root) { * * @deprecated use {@link #coercedVariables(CoercedVariables)} instead */ - @Deprecated - @DeprecatedAt("2022-05-24") + @Deprecated(since = "2022-05-24") public ExecutionContextBuilder variables(Map variables) { this.coercedVariables = CoercedVariables.of(variables); return this; diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index 884ca79179..08836d977f 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfo.java +++ b/src/main/java/graphql/execution/ExecutionStepInfo.java @@ -1,6 +1,5 @@ package graphql.execution; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.collect.ImmutableMapWithNullValues; import graphql.schema.GraphQLFieldDefinition; @@ -84,8 +83,7 @@ private ExecutionStepInfo(Builder builder) { * @see ExecutionStepInfo#getObjectType() * @deprecated use {@link #getObjectType()} instead as it is named better */ - @Deprecated - @DeprecatedAt("2022-02-03") + @Deprecated(since = "2022-02-03") public GraphQLObjectType getFieldContainer() { return fieldContainer; } diff --git a/src/main/java/graphql/execution/TypeResolutionParameters.java b/src/main/java/graphql/execution/TypeResolutionParameters.java index 88a8e2db69..4f20fac4fd 100644 --- a/src/main/java/graphql/execution/TypeResolutionParameters.java +++ b/src/main/java/graphql/execution/TypeResolutionParameters.java @@ -1,6 +1,5 @@ package graphql.execution; -import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.Internal; import graphql.TypeResolutionEnvironment; @@ -74,8 +73,7 @@ public static Builder newParameters() { * * @deprecated use {@link #getGraphQLContext()} instead */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public Object getContext() { return context; } @@ -125,8 +123,7 @@ public Builder schema(GraphQLSchema schema) { return this; } - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public Builder context(Object context) { this.context = context; return this; diff --git a/src/main/java/graphql/execution/directives/QueryDirectives.java b/src/main/java/graphql/execution/directives/QueryDirectives.java index a7fafdad9b..162b6f3c53 100644 --- a/src/main/java/graphql/execution/directives/QueryDirectives.java +++ b/src/main/java/graphql/execution/directives/QueryDirectives.java @@ -1,6 +1,5 @@ package graphql.execution.directives; -import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.CoercedVariables; @@ -56,8 +55,7 @@ public interface QueryDirectives { * * @deprecated - use the {@link QueryAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") Map> getImmediateDirectivesByName(); /** @@ -79,8 +77,7 @@ public interface QueryDirectives { * * @deprecated - use the {@link QueryAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") List getImmediateDirective(String directiveName); /** @@ -91,8 +88,7 @@ public interface QueryDirectives { * * @deprecated - use the {@link QueryAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") Map> getImmediateDirectivesByField(); /** diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 77c4c6bd83..ae26e360e7 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation; -import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.PublicSpi; @@ -49,8 +48,7 @@ public interface Instrumentation { * * @deprecated use {@link #createState(InstrumentationCreateStateParameters)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") default InstrumentationState createState() { return null; } @@ -63,8 +61,7 @@ default InstrumentationState createState() { * * @return a state object that is passed to each method */ - @Deprecated - @DeprecatedAt("2023-08-25") + @Deprecated(since = "2023-08-25") @Nullable default InstrumentationState createState(InstrumentationCreateStateParameters parameters) { return createState(); @@ -92,8 +89,7 @@ default CompletableFuture createStateAsync(Instrumentation * * @deprecated use {@link #beginExecution(InstrumentationExecutionParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { return noOp(); @@ -121,8 +117,7 @@ default InstrumentationContext beginExecution(InstrumentationEx * * @deprecated use {@link #beginParse(InstrumentationExecutionParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { return noOp(); @@ -150,8 +145,7 @@ default InstrumentationContext beginParse(InstrumentationExecutionPara * * @deprecated use {@link #beginValidation(InstrumentationValidationParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { return noOp(); @@ -179,8 +173,7 @@ default InstrumentationContext> beginValidation(Instrument * * @deprecated use {@link #beginExecuteOperation(InstrumentationExecuteOperationParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { return noOp(); @@ -209,8 +202,7 @@ default InstrumentationContext beginExecuteOperation(Instrument * * @deprecated use {@link #beginExecutionStrategy(InstrumentationExecutionStrategyParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { return ExecutionStrategyInstrumentationContext.NOOP; @@ -240,8 +232,7 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen * * @deprecated use {@link #beginSubscribedFieldEvent(InstrumentationFieldParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { return noOp(); @@ -269,8 +260,7 @@ default InstrumentationContext beginSubscribedFieldEvent(Instru * * @deprecated use {@link #beginField(InstrumentationFieldParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginField(InstrumentationFieldParameters parameters) { return noOp(); @@ -298,8 +288,7 @@ default InstrumentationContext beginField(InstrumentationFieldP * * @deprecated use {@link #beginFieldFetch(InstrumentationFieldFetchParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { return noOp(); @@ -328,8 +317,7 @@ default InstrumentationContext beginFieldFetch(InstrumentationFieldFetch * * @deprecated use {@link #beginFieldComplete(InstrumentationFieldCompleteParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { return noOp(); @@ -357,8 +345,7 @@ default InstrumentationContext beginFieldComplete(Instrumentati * * @deprecated use {@link #beginFieldListComplete(InstrumentationFieldCompleteParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { return noOp(); @@ -388,8 +375,7 @@ default InstrumentationContext beginFieldListComplete(Instrumen * * @deprecated use {@link #instrumentExecutionInput(ExecutionInput, InstrumentationExecutionParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { return executionInput; @@ -420,8 +406,7 @@ default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, I * * @deprecated use {@link #instrumentDocumentAndVariables(DocumentAndVariables, InstrumentationExecutionParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { return documentAndVariables; @@ -452,8 +437,7 @@ default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables * * @deprecated use {@link #instrumentSchema(GraphQLSchema, InstrumentationExecutionParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { return schema; @@ -485,8 +469,7 @@ default GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExec * * @deprecated use {@link #instrumentExecutionContext(ExecutionContext, InstrumentationExecutionParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { return executionContext; @@ -507,7 +490,6 @@ default ExecutionContext instrumentExecutionContext(ExecutionContext executionCo return instrumentExecutionContext(executionContext, parameters.withNewState(state)); } - /** * This is called to instrument a {@link DataFetcher} just before it is used to fetch a field, allowing you * to adjust what information is passed back or record information about specific data fetches. Note @@ -521,8 +503,7 @@ default ExecutionContext instrumentExecutionContext(ExecutionContext executionCo * * @deprecated use {@link #instrumentDataFetcher(DataFetcher, InstrumentationFieldFetchParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { return dataFetcher; @@ -555,8 +536,7 @@ default DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrum * * @deprecated use {@link #instrumentExecutionResult(ExecutionResult, InstrumentationExecutionParameters, InstrumentationState)} instead */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @NotNull default CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { return CompletableFuture.completedFuture(executionResult); diff --git a/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java index f35278c551..d2df536e75 100644 --- a/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation; -import graphql.DeprecatedAt; import graphql.PublicApi; /** @@ -12,8 +11,7 @@ * @deprecated use {@link SimplePerformantInstrumentation} instead as a base class. */ @PublicApi -@Deprecated -@DeprecatedAt(value = "2022-10-05") +@Deprecated(since = "2022-10-05") public class SimpleInstrumentation implements Instrumentation { /** diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java index f8a895657b..4de6a1164c 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation.parameters; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.instrumentation.Instrumentation; @@ -33,8 +32,7 @@ private InstrumentationExecuteOperationParameters(ExecutionContext executionCont * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") public InstrumentationExecuteOperationParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationExecuteOperationParameters(executionContext, instrumentationState); } @@ -53,8 +51,7 @@ public ExecutionContext getExecutionContext() { * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") public T getInstrumentationState() { //noinspection unchecked return (T) instrumentationState; diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java index 57ec489851..10cce06031 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation.parameters; -import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.PublicApi; @@ -45,8 +44,7 @@ public InstrumentationExecutionParameters(ExecutionInput executionInput, GraphQL * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") public InstrumentationExecutionParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationExecutionParameters(this.getExecutionInput(), this.schema, instrumentationState); } @@ -70,8 +68,7 @@ public String getOperation() { * * @deprecated use {@link #getGraphQLContext()} instead */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public T getContext() { return (T) context; @@ -95,8 +92,7 @@ public Map getVariables() { * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java index 7f1ec801b3..a2ce253cdc 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation.parameters; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; @@ -36,8 +35,7 @@ private InstrumentationExecutionStrategyParameters(ExecutionContext executionCon * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") public InstrumentationExecutionStrategyParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationExecutionStrategyParameters(executionContext, executionStrategyParameters, instrumentationState); } @@ -60,8 +58,7 @@ public ExecutionStrategyParameters getExecutionStrategyParameters() { * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java index 254e72f47b..dd5d2244d4 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation.parameters; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStepInfo; @@ -43,8 +42,7 @@ public InstrumentationFieldCompleteParameters(ExecutionContext executionContext, * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") public InstrumentationFieldCompleteParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationFieldCompleteParameters( this.executionContext, executionStrategyParameters, this.executionStepInfo, this.fetchedValue, instrumentationState); @@ -63,8 +61,7 @@ public GraphQLFieldDefinition getField() { return getExecutionStepInfo().getFieldDefinition(); } - @Deprecated - @DeprecatedAt("2020-09-08") + @Deprecated(since = "2020-09-08") public ExecutionStepInfo getTypeInfo() { return getExecutionStepInfo(); } @@ -87,8 +84,7 @@ public Object getFetchedValue() { * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java index 6013214013..5c54cd3e98 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation.parameters; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; @@ -42,8 +41,7 @@ private InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @Override public InstrumentationFieldFetchParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationFieldFetchParameters( diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java index 070def45a1..9c6342d2c7 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation.parameters; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStepInfo; @@ -38,8 +37,7 @@ public InstrumentationFieldParameters(ExecutionContext executionContext, Supplie * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") public InstrumentationFieldParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationFieldParameters( this.executionContext, this.executionStepInfo, instrumentationState); @@ -68,8 +66,7 @@ public ExecutionStepInfo getExecutionStepInfo() { * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java index a99619affa..713b75c63c 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java @@ -1,6 +1,5 @@ package graphql.execution.instrumentation.parameters; -import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.PublicApi; import graphql.execution.instrumentation.Instrumentation; @@ -29,8 +28,7 @@ public InstrumentationValidationParameters(ExecutionInput executionInput, Docume * * @deprecated state is now passed in direct to instrumentation methods */ - @Deprecated - @DeprecatedAt("2022-07-26") + @Deprecated(since = "2022-07-26") @Override public InstrumentationValidationParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationValidationParameters( diff --git a/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java b/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java index a9d8f1e842..402b401e43 100644 --- a/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java +++ b/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java @@ -1,7 +1,6 @@ package graphql.execution.preparsed; -import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.PublicSpi; @@ -27,8 +26,7 @@ public interface PreparsedDocumentProvider { *

* @deprecated - use {@link #getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction)} */ - @Deprecated - @DeprecatedAt("2021-12-06") + @Deprecated(since = "2021-12-06") PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction); /** diff --git a/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java b/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java index 034e4b4ebb..52d1e79a18 100644 --- a/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java +++ b/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java @@ -1,6 +1,5 @@ package graphql.execution.preparsed.persisted; -import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.PublicSpi; import graphql.execution.preparsed.PreparsedDocumentEntry; @@ -32,8 +31,7 @@ public interface PersistedQueryCache { * * @deprecated - use {@link #getPersistedQueryDocumentAsync(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss)} */ - @Deprecated - @DeprecatedAt("2021-12-06") + @Deprecated(since = "2021-12-06") PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound; /** diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index a2bf6b9058..a02ea9567a 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -1,7 +1,6 @@ package graphql.parser; import com.google.common.collect.ImmutableList; -import graphql.DeprecatedAt; import graphql.Internal; import graphql.PublicApi; import graphql.language.Document; @@ -167,8 +166,7 @@ public Document parseDocument(Reader reader) throws InvalidSyntaxException { * @throws InvalidSyntaxException if the input is not valid graphql syntax * @deprecated use {#{@link #parse(ParserEnvironment)}} instead */ - @DeprecatedAt("2022-08-31") - @Deprecated + @Deprecated(since = "2022-08-31") public Document parseDocument(String input, String sourceName) throws InvalidSyntaxException { MultiSourceReader multiSourceReader = MultiSourceReader.newMultiSourceReader() .string(input, sourceName) @@ -188,8 +186,7 @@ public Document parseDocument(String input, String sourceName) throws InvalidSyn * @throws InvalidSyntaxException if the input is not valid graphql syntax * @deprecated use {#{@link #parse(ParserEnvironment)}} instead */ - @DeprecatedAt("2022-08-31") - @Deprecated + @Deprecated(since = "2022-08-31") public Document parseDocument(String input, ParserOptions parserOptions) throws InvalidSyntaxException { MultiSourceReader multiSourceReader = MultiSourceReader.newMultiSourceReader() .string(input, null) @@ -209,8 +206,7 @@ public Document parseDocument(String input, ParserOptions parserOptions) throws * @throws InvalidSyntaxException if the input is not valid graphql syntax * @deprecated use {#{@link #parse(ParserEnvironment)}} instead */ - @DeprecatedAt("2022-08-31") - @Deprecated + @Deprecated(since = "2022-08-31") public Document parseDocument(Reader reader, ParserOptions parserOptions) throws InvalidSyntaxException { ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() .document(reader) diff --git a/src/main/java/graphql/schema/Coercing.java b/src/main/java/graphql/schema/Coercing.java index 0b4d127b80..cf8de535a5 100644 --- a/src/main/java/graphql/schema/Coercing.java +++ b/src/main/java/graphql/schema/Coercing.java @@ -1,7 +1,6 @@ package graphql.schema; -import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicSpi; import graphql.execution.CoercedVariables; @@ -54,8 +53,7 @@ public interface Coercing { * * @throws graphql.schema.CoercingSerializeException if value input can't be serialized */ - @Deprecated - @DeprecatedAt("2022-08-22") + @Deprecated(since = "2022-08-22") default @Nullable O serialize(@NotNull Object dataFetcherResult) throws CoercingSerializeException { throw new UnsupportedOperationException("The non deprecated version of serialize has not been implemented by this scalar : " + this.getClass()); } @@ -99,8 +97,7 @@ public interface Coercing { * * @throws graphql.schema.CoercingParseValueException if value input can't be parsed */ - @Deprecated - @DeprecatedAt("2022-08-22") + @Deprecated(since = "2022-08-22") default @Nullable I parseValue(@NotNull Object input) throws CoercingParseValueException { throw new UnsupportedOperationException("The non deprecated version of parseValue has not been implemented by this scalar : " + this.getClass()); } @@ -146,8 +143,7 @@ default I parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLConte * * @throws graphql.schema.CoercingParseLiteralException if input literal can't be parsed */ - @Deprecated - @DeprecatedAt("2022-08-22") + @Deprecated(since = "2022-08-22") default @Nullable I parseLiteral(@NotNull Object input) throws CoercingParseLiteralException { throw new UnsupportedOperationException("The non deprecated version of parseLiteral has not been implemented by this scalar : " + this.getClass()); } @@ -175,8 +171,7 @@ default I parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLConte * @throws graphql.schema.CoercingParseLiteralException if input literal can't be parsed */ @SuppressWarnings("unused") - @Deprecated - @DeprecatedAt("2022-08-22") + @Deprecated(since = "2022-08-22") default @Nullable I parseLiteral(Object input, Map variables) throws CoercingParseLiteralException { return parseLiteral(input); } @@ -222,8 +217,7 @@ default I parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLConte * * @return The literal matching the external input value. */ - @Deprecated - @DeprecatedAt("2022-08-22") + @Deprecated(since = "2022-08-22") default @NotNull Value valueToLiteral(@NotNull Object input) { throw new UnsupportedOperationException("The non deprecated version of valueToLiteral has not been implemented by this scalar : " + this.getClass()); } diff --git a/src/main/java/graphql/schema/DataFetchingEnvironment.java b/src/main/java/graphql/schema/DataFetchingEnvironment.java index 041d6a9ca2..ce46ad75c9 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironment.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironment.java @@ -1,6 +1,5 @@ package graphql.schema; -import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.ExecutionId; @@ -87,8 +86,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * * @deprecated - use {@link #getGraphQlContext()} instead */ - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") T getContext(); /** @@ -138,8 +136,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * * @deprecated Use {@link #getMergedField()}. */ - @Deprecated - @DeprecatedAt("2018-12-20") + @Deprecated(since = "2018-12-20") List getFields(); /** diff --git a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java index dbf9618a43..d94197141e 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableMap; -import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.Internal; import graphql.collect.ImmutableKit; @@ -306,8 +305,7 @@ public Builder arguments(Supplier> arguments) { return this; } - @Deprecated - @DeprecatedAt("2021-07-05") + @Deprecated(since = "2021-07-05") public Builder context(Object context) { this.context = context; return this; diff --git a/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java b/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java index ff10e1cdd7..41d6795556 100644 --- a/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java +++ b/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java @@ -1,6 +1,5 @@ package graphql.schema; -import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.ExecutionId; @@ -64,8 +63,7 @@ public T getArgumentOrDefault(String name, T defaultValue) { return delegateEnvironment.getArgumentOrDefault(name, defaultValue); } - @Deprecated - @DeprecatedAt("2022-04-17") + @Deprecated(since = "2022-04-17") @Override public T getContext() { return delegateEnvironment.getContext(); @@ -91,8 +89,7 @@ public GraphQLFieldDefinition getFieldDefinition() { return delegateEnvironment.getFieldDefinition(); } - @Deprecated - @DeprecatedAt("2019-10-07") + @Deprecated(since = "2019-10-07") @Override public List getFields() { return delegateEnvironment.getFields(); diff --git a/src/main/java/graphql/schema/GraphQLArgument.java b/src/main/java/graphql/schema/GraphQLArgument.java index 4ba3f123ea..99919b48b2 100644 --- a/src/main/java/graphql/schema/GraphQLArgument.java +++ b/src/main/java/graphql/schema/GraphQLArgument.java @@ -1,7 +1,6 @@ package graphql.schema; -import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.GraphQLContext; import graphql.PublicApi; @@ -125,8 +124,7 @@ public boolean hasSetValue() { * * @deprecated use {@link GraphQLAppliedDirectiveArgument} instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public @NotNull InputValueWithState getArgumentValue() { return value; } @@ -149,8 +147,7 @@ public boolean hasSetValue() { * * @deprecated use {@link GraphQLAppliedDirectiveArgument} instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public static T getArgumentValue(GraphQLArgument argument) { return getInputValueImpl(argument.getType(), argument.getArgumentValue(), GraphQLContext.getDefault(), Locale.getDefault()); } @@ -374,8 +371,7 @@ public Builder type(GraphQLInputType type) { * * @deprecated use {@link #defaultValueLiteral(Value)} or {@link #defaultValueProgrammatic(Object)} */ - @Deprecated - @DeprecatedAt("2021-05-10") + @Deprecated(since = "2021-05-10") public Builder defaultValue(Object defaultValue) { this.defaultValue = InputValueWithState.newInternalValue(defaultValue); return this; @@ -420,8 +416,7 @@ public Builder clearDefaultValue() { * * @deprecated use {@link #valueLiteral(Value)} or {@link #valueProgrammatic(Object)} */ - @Deprecated - @DeprecatedAt("2021-05-10") + @Deprecated(since = "2021-05-10") public Builder value(@Nullable Object value) { this.value = InputValueWithState.newInternalValue(value); return this; @@ -436,8 +431,7 @@ public Builder value(@Nullable Object value) { * * @deprecated use {@link GraphQLAppliedDirectiveArgument} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public Builder valueLiteral(@NotNull Value value) { this.value = InputValueWithState.newLiteralValue(value); return this; @@ -450,8 +444,7 @@ public Builder valueLiteral(@NotNull Value value) { * * @deprecated use {@link GraphQLAppliedDirectiveArgument} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public Builder valueProgrammatic(@Nullable Object value) { this.value = InputValueWithState.newExternalValue(value); return this; @@ -464,8 +457,7 @@ public Builder valueProgrammatic(@Nullable Object value) { * * @deprecated use {@link GraphQLAppliedDirectiveArgument} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public Builder clearValue() { this.value = InputValueWithState.NOT_SET; return this; diff --git a/src/main/java/graphql/schema/GraphQLCodeRegistry.java b/src/main/java/graphql/schema/GraphQLCodeRegistry.java index 9620ca19a7..d1b0f94d4c 100644 --- a/src/main/java/graphql/schema/GraphQLCodeRegistry.java +++ b/src/main/java/graphql/schema/GraphQLCodeRegistry.java @@ -1,7 +1,6 @@ package graphql.schema; import graphql.Assert; -import graphql.DeprecatedAt; import graphql.Internal; import graphql.PublicApi; import graphql.schema.visibility.GraphqlFieldVisibility; @@ -62,8 +61,7 @@ public GraphqlFieldVisibility getFieldVisibility() { * @deprecated This is confusing because {@link GraphQLInterfaceType}s cant have data fetchers. At runtime only a {@link GraphQLObjectType} * can be used to fetch a field. This method allows the mapping to be made, but it is never useful if an interface is passed in. */ - @Deprecated - @DeprecatedAt("2023-05-13") + @Deprecated(since = "2023-05-13") public DataFetcher getDataFetcher(GraphQLFieldsContainer parentType, GraphQLFieldDefinition fieldDefinition) { return getDataFetcherImpl(FieldCoordinates.coordinates(parentType, fieldDefinition), fieldDefinition, dataFetcherMap, systemDataFetcherMap, defaultDataFetcherFactory); } @@ -266,8 +264,7 @@ private Builder markChanged(boolean condition) { * @deprecated This is confusing because {@link GraphQLInterfaceType}s cant have data fetchers. At runtime only a {@link GraphQLObjectType} * can be used to fetch a field. This method allows the mapping to be made, but it is never useful if an interface is passed in. */ - @Deprecated - @DeprecatedAt("2023-05-13") + @Deprecated(since = "2023-05-13") public DataFetcher getDataFetcher(GraphQLFieldsContainer parentType, GraphQLFieldDefinition fieldDefinition) { return getDataFetcherImpl(FieldCoordinates.coordinates(parentType, fieldDefinition), fieldDefinition, dataFetcherMap, systemDataFetcherMap, defaultDataFetcherFactory); } @@ -373,13 +370,11 @@ public Builder dataFetcher(FieldCoordinates coordinates, DataFetcher dataFetc * @deprecated This is confusing because {@link GraphQLInterfaceType}s cant have data fetchers. At runtime only a {@link GraphQLObjectType} * can be used to fetch a field. This method allows the mapping to be made, but it is never useful if an interface is passed in. */ - @Deprecated - @DeprecatedAt("2023-05-13") + @Deprecated(since = "2023-05-13") public Builder dataFetcher(GraphQLFieldsContainer parentType, GraphQLFieldDefinition fieldDefinition, DataFetcher dataFetcher) { return dataFetcher(FieldCoordinates.coordinates(parentType.getName(), fieldDefinition.getName()), dataFetcher); } - /** * Sets the data fetcher for a specific field inside an object type * diff --git a/src/main/java/graphql/schema/GraphQLDirectiveContainer.java b/src/main/java/graphql/schema/GraphQLDirectiveContainer.java index 52caebef29..4bee55aa36 100644 --- a/src/main/java/graphql/schema/GraphQLDirectiveContainer.java +++ b/src/main/java/graphql/schema/GraphQLDirectiveContainer.java @@ -1,6 +1,5 @@ package graphql.schema; -import graphql.DeprecatedAt; import graphql.PublicApi; import java.util.List; @@ -76,8 +75,7 @@ default List getAppliedDirectives(String directiveName) * * @deprecated use {@link #hasAppliedDirective(String)} instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") default boolean hasDirective(String directiveName) { return getAllDirectivesByName().containsKey(directiveName); } @@ -101,8 +99,7 @@ default boolean hasAppliedDirective(String directiveName) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") List getDirectives(); /** @@ -113,8 +110,7 @@ default boolean hasAppliedDirective(String directiveName) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") Map getDirectivesByName(); /** @@ -125,8 +121,7 @@ default boolean hasAppliedDirective(String directiveName) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") Map> getAllDirectivesByName(); /** @@ -139,8 +134,7 @@ default boolean hasAppliedDirective(String directiveName) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") GraphQLDirective getDirective(String directiveName); /** @@ -152,8 +146,7 @@ default boolean hasAppliedDirective(String directiveName) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") default List getDirectives(String directiveName) { return getAllDirectivesByName().getOrDefault(directiveName, emptyList()); } diff --git a/src/main/java/graphql/schema/GraphQLFieldDefinition.java b/src/main/java/graphql/schema/GraphQLFieldDefinition.java index 79f824997d..c717c929a2 100644 --- a/src/main/java/graphql/schema/GraphQLFieldDefinition.java +++ b/src/main/java/graphql/schema/GraphQLFieldDefinition.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; -import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.Internal; import graphql.PublicApi; @@ -88,8 +87,7 @@ public GraphQLOutputType getType() { // to be removed in a future version when all code is in the code registry @Internal - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") DataFetcher getDataFetcher() { if (dataFetcherFactory == null) { return null; @@ -310,8 +308,7 @@ public Builder type(GraphQLOutputType type) { * * @deprecated use {@link graphql.schema.GraphQLCodeRegistry} instead */ - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") public Builder dataFetcher(DataFetcher dataFetcher) { assertNotNull(dataFetcher, () -> "dataFetcher must be not null"); this.dataFetcherFactory = DataFetcherFactories.useDataFetcher(dataFetcher); @@ -327,8 +324,7 @@ public Builder dataFetcher(DataFetcher dataFetcher) { * * @deprecated use {@link graphql.schema.GraphQLCodeRegistry} instead */ - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") public Builder dataFetcherFactory(DataFetcherFactory dataFetcherFactory) { assertNotNull(dataFetcherFactory, () -> "dataFetcherFactory must be not null"); this.dataFetcherFactory = dataFetcherFactory; @@ -344,8 +340,7 @@ public Builder dataFetcherFactory(DataFetcherFactory dataFetcherFactory) { * * @deprecated use {@link graphql.schema.GraphQLCodeRegistry} instead */ - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") public Builder staticValue(final Object value) { this.dataFetcherFactory = DataFetcherFactories.useDataFetcher(environment -> value); return this; @@ -398,8 +393,7 @@ public Builder argument(GraphQLArgument.Builder builder) { * * @deprecated This is a badly named method and is replaced by {@link #arguments(java.util.List)} */ - @Deprecated - @DeprecatedAt("2019-02-06") + @Deprecated(since = "2019-02-06") public Builder argument(List arguments) { return arguments(arguments); } diff --git a/src/main/java/graphql/schema/GraphQLInputObjectField.java b/src/main/java/graphql/schema/GraphQLInputObjectField.java index fda458defc..9a9aa0a85f 100644 --- a/src/main/java/graphql/schema/GraphQLInputObjectField.java +++ b/src/main/java/graphql/schema/GraphQLInputObjectField.java @@ -1,7 +1,6 @@ package graphql.schema; -import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.GraphQLContext; import graphql.PublicApi; @@ -315,8 +314,7 @@ public Builder type(GraphQLInputType type) { * * @deprecated use {@link #defaultValueLiteral(Value)} */ - @Deprecated - @DeprecatedAt("2021-05-10") + @Deprecated(since = "2021-05-10") public Builder defaultValue(Object defaultValue) { this.defaultValue = InputValueWithState.newInternalValue(defaultValue); return this; diff --git a/src/main/java/graphql/schema/GraphQLInterfaceType.java b/src/main/java/graphql/schema/GraphQLInterfaceType.java index 814609a7c5..238b5abdd3 100644 --- a/src/main/java/graphql/schema/GraphQLInterfaceType.java +++ b/src/main/java/graphql/schema/GraphQLInterfaceType.java @@ -3,7 +3,6 @@ import com.google.common.collect.ImmutableList; import graphql.Assert; import graphql.AssertException; -import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.Internal; import graphql.PublicApi; @@ -114,8 +113,7 @@ public String getDescription() { // to be removed in a future version when all code is in the code registry @Internal - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") TypeResolver getTypeResolver() { return typeResolver; } @@ -370,8 +368,7 @@ public Builder clearFields() { * * @deprecated use {@link graphql.schema.GraphQLCodeRegistry.Builder#typeResolver(GraphQLInterfaceType, TypeResolver)} instead */ - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") public Builder typeResolver(TypeResolver typeResolver) { this.typeResolver = typeResolver; return this; diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 335c694af6..e365ef774b 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -5,7 +5,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import graphql.Assert; -import graphql.DeprecatedAt; import graphql.Directives; import graphql.DirectivesUtil; import graphql.Internal; @@ -418,8 +417,7 @@ public GraphQLObjectType getSubscriptionType() { * * @deprecated use {@link GraphQLCodeRegistry#getFieldVisibility()} instead */ - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") public GraphqlFieldVisibility getFieldVisibility() { return codeRegistry.getFieldVisibility(); } @@ -464,8 +462,7 @@ public GraphQLDirective getDirective(String directiveName) { * * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public List getSchemaDirectives() { return schemaAppliedDirectivesHolder.getDirectives(); } @@ -480,8 +477,7 @@ public List getSchemaDirectives() { * * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public Map getSchemaDirectiveByName() { return schemaAppliedDirectivesHolder.getDirectivesByName(); } @@ -496,8 +492,7 @@ public Map getSchemaDirectiveByName() { * * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public Map> getAllSchemaDirectivesByName() { return schemaAppliedDirectivesHolder.getAllDirectivesByName(); } @@ -514,8 +509,7 @@ public Map> getAllSchemaDirectivesByName() { * * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public GraphQLDirective getSchemaDirective(String directiveName) { return schemaAppliedDirectivesHolder.getDirective(directiveName); } @@ -530,8 +524,7 @@ public GraphQLDirective getSchemaDirective(String directiveName) { * * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public List getSchemaDirectives(String directiveName) { return schemaAppliedDirectivesHolder.getDirectives(directiveName); } @@ -742,8 +735,7 @@ public Builder subscription(GraphQLObjectType subscriptionType) { * * @deprecated use {@link graphql.schema.GraphQLCodeRegistry.Builder#fieldVisibility(graphql.schema.visibility.GraphqlFieldVisibility)} instead */ - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") public Builder fieldVisibility(GraphqlFieldVisibility fieldVisibility) { this.codeRegistry = this.codeRegistry.transform(builder -> builder.fieldVisibility(fieldVisibility)); return this; @@ -873,8 +865,7 @@ public Builder introspectionSchemaType(GraphQLObjectType introspectionSchemaType * * @deprecated - Use the {@link #additionalType(GraphQLType)} methods */ - @Deprecated - @DeprecatedAt("2018-07-30") + @Deprecated(since = "2018-07-30") public GraphQLSchema build(Set additionalTypes) { return additionalTypes(additionalTypes).build(); } @@ -889,8 +880,7 @@ public GraphQLSchema build(Set additionalTypes) { * * @deprecated - Use the {@link #additionalType(GraphQLType)} and {@link #additionalDirective(GraphQLDirective)} methods */ - @Deprecated - @DeprecatedAt("2018-07-30") + @Deprecated(since = "2018-07-30") public GraphQLSchema build(Set additionalTypes, Set additionalDirectives) { return additionalTypes(additionalTypes).additionalDirectives(additionalDirectives).build(); } diff --git a/src/main/java/graphql/schema/GraphQLUnionType.java b/src/main/java/graphql/schema/GraphQLUnionType.java index 452fcd2da9..6c5fffce39 100644 --- a/src/main/java/graphql/schema/GraphQLUnionType.java +++ b/src/main/java/graphql/schema/GraphQLUnionType.java @@ -3,7 +3,6 @@ import com.google.common.collect.ImmutableList; import graphql.Assert; -import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.Internal; import graphql.PublicApi; @@ -102,8 +101,7 @@ public boolean isPossibleType(GraphQLObjectType graphQLObjectType) { // to be removed in a future version when all code is in the code registry @Internal - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") TypeResolver getTypeResolver() { return typeResolver; } @@ -282,14 +280,12 @@ public Builder extensionDefinitions(List extension * * @deprecated use {@link graphql.schema.GraphQLCodeRegistry.Builder#typeResolver(GraphQLUnionType, TypeResolver)} instead */ - @Deprecated - @DeprecatedAt("2018-12-03") + @Deprecated(since = "2018-12-03") public Builder typeResolver(TypeResolver typeResolver) { this.typeResolver = typeResolver; return this; } - public Builder possibleType(GraphQLObjectType type) { assertNotNull(type, () -> "possible type can't be null"); types.put(type.getName(), type); diff --git a/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java b/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java index b42d2eb03a..b9db03ebb0 100644 --- a/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java +++ b/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java @@ -1,6 +1,5 @@ package graphql.schema; -import graphql.DeprecatedAt; import graphql.Internal; import java.util.ArrayList; @@ -50,8 +49,7 @@ public B withAppliedDirective(GraphQLAppliedDirective.Builder builder) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public B replaceDirectives(List directives) { assertNotNull(directives, () -> "directive can't be null"); this.directives.clear(); @@ -66,8 +64,7 @@ public B replaceDirectives(List directives) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public B withDirectives(GraphQLDirective... directives) { assertNotNull(directives, () -> "directives can't be null"); this.directives.clear(); @@ -84,8 +81,7 @@ public B withDirectives(GraphQLDirective... directives) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public B withDirective(GraphQLDirective directive) { assertNotNull(directive, () -> "directive can't be null"); this.directives.add(directive); @@ -99,8 +95,7 @@ public B withDirective(GraphQLDirective directive) { * * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ - @Deprecated - @DeprecatedAt("2022-02-24") + @Deprecated(since = "2022-02-24") public B withDirective(GraphQLDirective.Builder builder) { return withDirective(builder.build()); } diff --git a/src/main/java/graphql/schema/diff/DiffSet.java b/src/main/java/graphql/schema/diff/DiffSet.java index 3823c30d59..68c06aa13d 100644 --- a/src/main/java/graphql/schema/diff/DiffSet.java +++ b/src/main/java/graphql/schema/diff/DiffSet.java @@ -1,7 +1,6 @@ package graphql.schema.diff; import graphql.Assert; -import graphql.DeprecatedAt; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.PublicApi; @@ -16,8 +15,7 @@ * {@link graphql.introspection.IntrospectionQuery}. */ @PublicApi -@Deprecated -@DeprecatedAt("2023-10-04") +@Deprecated(since = "2023-10-04") public class DiffSet { private final Map introspectionOld; diff --git a/src/main/java/graphql/schema/diff/SchemaDiff.java b/src/main/java/graphql/schema/diff/SchemaDiff.java index 9012c217f9..72161dfde3 100644 --- a/src/main/java/graphql/schema/diff/SchemaDiff.java +++ b/src/main/java/graphql/schema/diff/SchemaDiff.java @@ -1,7 +1,6 @@ package graphql.schema.diff; import graphql.Assert; -import graphql.DeprecatedAt; import graphql.PublicSpi; import graphql.introspection.IntrospectionResultToSchema; import graphql.language.Argument; @@ -121,8 +120,7 @@ public SchemaDiff(Options options) { * * @return the number of API breaking changes */ - @Deprecated - @DeprecatedAt("2023-10-04") + @Deprecated(since = "2023-10-04") @SuppressWarnings("unchecked") public int diffSchema(DiffSet diffSet, DifferenceReporter reporter) { CountingReporter countingReporter = new CountingReporter(reporter); diff --git a/src/main/java/graphql/schema/idl/RuntimeWiring.java b/src/main/java/graphql/schema/idl/RuntimeWiring.java index 69ba9f6633..52f909ac63 100644 --- a/src/main/java/graphql/schema/idl/RuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/RuntimeWiring.java @@ -1,6 +1,5 @@ package graphql.schema.idl; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.schema.DataFetcher; import graphql.schema.GraphQLCodeRegistry; @@ -356,8 +355,7 @@ public Builder comparatorRegistry(GraphqlTypeComparatorRegistry comparatorRegist * @deprecated This mechanism can be achieved in a better way via {@link graphql.schema.SchemaTransformer} * after the schema is built */ - @Deprecated - @DeprecatedAt(value = "2022-10-29") + @Deprecated(since = "2022-10-29") public Builder transformer(SchemaGeneratorPostProcessing schemaGeneratorPostProcessing) { this.schemaGeneratorPostProcessings.add(assertNotNull(schemaGeneratorPostProcessing)); return this; diff --git a/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java b/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java index dfe09acb43..e888039308 100644 --- a/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java +++ b/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java @@ -1,6 +1,5 @@ package graphql.schema.idl; -import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.language.NamedNode; import graphql.language.NodeParentTree; @@ -43,8 +42,7 @@ public interface SchemaDirectiveWiringEnvironment getDirectives(); /** @@ -79,8 +76,7 @@ public interface SchemaDirectiveWiringEnvironment Date: Mon, 1 Jan 2024 10:49:27 +0900 Subject: [PATCH 060/393] Fix test --- .../groovy/example/http/ExecutionResultJSONTesting.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/example/http/ExecutionResultJSONTesting.java b/src/test/groovy/example/http/ExecutionResultJSONTesting.java index 965ac68e4b..019eb17616 100644 --- a/src/test/groovy/example/http/ExecutionResultJSONTesting.java +++ b/src/test/groovy/example/http/ExecutionResultJSONTesting.java @@ -70,7 +70,11 @@ private void testGson(HttpServletResponse response, Object er) throws IOExceptio private ExecutionResult createER() { List errors = new ArrayList<>(); - errors.add(new ValidationError(ValidationErrorType.UnknownType, mkLocations(), "Test ValidationError")); // Retain as there is no alternative constructor for ValidationError + errors.add(ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.UnknownType) + .sourceLocations(mkLocations()) + .description("Test ValidationError") + .build()); errors.add(new MissingRootTypeException("Mutations are not supported.", null)); errors.add(new InvalidSyntaxError(mkLocations(), "Not good syntax m'kay")); errors.add(new NonNullableFieldWasNullError(new NonNullableFieldWasNullException(mkExecutionInfo(), mkPath()))); From 3c811c5986d37e2a952a699efd118ca7031dfaa1 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:54:08 +0900 Subject: [PATCH 061/393] Untangle two PRs, separately remove these ValidationError constructors in different PR --- .../graphql/validation/ValidationError.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/main/java/graphql/validation/ValidationError.java b/src/main/java/graphql/validation/ValidationError.java index 04c1f88936..7f456e0037 100644 --- a/src/main/java/graphql/validation/ValidationError.java +++ b/src/main/java/graphql/validation/ValidationError.java @@ -23,6 +23,46 @@ public class ValidationError implements GraphQLError { private final List queryPath = new ArrayList<>(); private final ImmutableMap extensions; + @Deprecated(since = "2022-07-10", forRemoval = true) + public ValidationError(ValidationErrorClassification validationErrorType) { + this(newValidationError() + .validationErrorType(validationErrorType)); + } + + @Deprecated(since = "2022-07-10", forRemoval = true) + public ValidationError(ValidationErrorClassification validationErrorType, SourceLocation sourceLocation, String description) { + this(newValidationError() + .validationErrorType(validationErrorType) + .sourceLocation(sourceLocation) + .description(description)); + } + + @Deprecated(since = "2022-07-10", forRemoval = true) + public ValidationError(ValidationErrorType validationErrorType, SourceLocation sourceLocation, String description, List queryPath) { + this(newValidationError() + .validationErrorType(validationErrorType) + .sourceLocation(sourceLocation) + .description(description) + .queryPath(queryPath)); + } + + @Deprecated(since = "2022-07-10", forRemoval = true) + public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description) { + this(newValidationError() + .validationErrorType(validationErrorType) + .sourceLocations(sourceLocations) + .description(description)); + } + + @Deprecated(since = "2022-07-10", forRemoval = true) + public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description, List queryPath) { + this(newValidationError() + .validationErrorType(validationErrorType) + .sourceLocations(sourceLocations) + .description(description) + .queryPath(queryPath)); + } + private ValidationError(Builder builder) { this.validationErrorType = builder.validationErrorType; this.description = builder.description; From 7fb726238acf2a9314c4ff6e6de13932765a68e9 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Jan 2024 11:13:52 +0900 Subject: [PATCH 062/393] Fix Groovy tests --- .../graphql/validation/ValidationError.java | 1 - src/test/groovy/graphql/ErrorsTest.groovy | 18 +++++++++++++++--- .../groovy/graphql/GraphQLErrorTest.groovy | 6 +++++- .../graphql/GraphqlErrorHelperTest.groovy | 6 +++++- .../execution/DataFetcherResultTest.groovy | 2 +- ...tegyExceptionHandlingEquivalenceTest.groovy | 2 +- .../PreparsedDocumentEntryTest.groovy | 2 +- .../graphql/language/SerialisationTest.groovy | 12 ++++++++++-- 8 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/main/java/graphql/validation/ValidationError.java b/src/main/java/graphql/validation/ValidationError.java index b3b27d8477..04c1f88936 100644 --- a/src/main/java/graphql/validation/ValidationError.java +++ b/src/main/java/graphql/validation/ValidationError.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableMap; -import graphql.DeprecatedAt; import graphql.ErrorType; import graphql.GraphQLError; import graphql.GraphqlErrorHelper; diff --git a/src/test/groovy/graphql/ErrorsTest.groovy b/src/test/groovy/graphql/ErrorsTest.groovy index dfae441761..e346a26b47 100644 --- a/src/test/groovy/graphql/ErrorsTest.groovy +++ b/src/test/groovy/graphql/ErrorsTest.groovy @@ -43,9 +43,21 @@ class ErrorsTest extends Specification { def "ValidationError equals and hashcode works"() { expect: - def same1 = new ValidationError(ValidationErrorType.BadValueForDefaultArg, [src(15, 34), src(23, 567)], "bad ju ju") - def same2 = new ValidationError(ValidationErrorType.BadValueForDefaultArg, [src(15, 34), src(23, 567)], "bad ju ju") - def different1 = new ValidationError(ValidationErrorType.FieldsConflict, [src(15, 34), src(23, 567)], "bad ju ju") + def same1 = ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.BadValueForDefaultArg) + .sourceLocations([src(15, 34), src(23, 567)]) + .description("bad ju ju") + .build() + def same2 = ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.BadValueForDefaultArg) + .sourceLocations([src(15, 34), src(23, 567)]) + .description("bad ju ju") + .build() + def different1 = ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.FieldsConflict) + .sourceLocations([src(15, 34), src(23, 567)]) + .description("bad ju ju") + .build() commonAssert(same1, same2, different1) } diff --git a/src/test/groovy/graphql/GraphQLErrorTest.groovy b/src/test/groovy/graphql/GraphQLErrorTest.groovy index 7560419be8..ca9fc1e8d7 100644 --- a/src/test/groovy/graphql/GraphQLErrorTest.groovy +++ b/src/test/groovy/graphql/GraphQLErrorTest.groovy @@ -25,7 +25,11 @@ class GraphQLErrorTest extends Specification { where: gError | expectedMap - new ValidationError(ValidationErrorType.UnknownType, mkLocations(), "Test ValidationError") | + ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.UnknownType) + .sourceLocations(mkLocations()) + .description("Test ValidationError") + .build() | [ locations: [[line: 666, column: 999], [line: 333, column: 0]], message : "Test ValidationError", diff --git a/src/test/groovy/graphql/GraphqlErrorHelperTest.groovy b/src/test/groovy/graphql/GraphqlErrorHelperTest.groovy index 34684b6ce6..88bfecb444 100644 --- a/src/test/groovy/graphql/GraphqlErrorHelperTest.groovy +++ b/src/test/groovy/graphql/GraphqlErrorHelperTest.groovy @@ -65,7 +65,11 @@ class GraphqlErrorHelperTest extends Specification { def "can turn error classifications into extensions"() { - def validationErr = new ValidationError(ValidationErrorType.InvalidFragmentType, new SourceLocation(6, 9), "Things are not valid") + def validationErr = ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.InvalidFragmentType) + .sourceLocation(new SourceLocation(6, 9)) + .description("Things are not valid") + .build() when: def specMap = GraphqlErrorHelper.toSpecification(validationErr) diff --git a/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy b/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy index a7bc5b7fa7..35fbfe2f1d 100644 --- a/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy +++ b/src/test/groovy/graphql/execution/DataFetcherResultTest.groovy @@ -7,7 +7,7 @@ import spock.lang.Specification class DataFetcherResultTest extends Specification { - def error1 = new ValidationError(ValidationErrorType.DuplicateOperationName) + def error1 = ValidationError.newValidationError().validationErrorType(ValidationErrorType.DuplicateOperationName).build() def error2 = new InvalidSyntaxError([], "Boo") def "basic building"() { diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy index af179821cb..2c8653491e 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy @@ -18,7 +18,7 @@ class ExecutionStrategyExceptionHandlingEquivalenceTest extends Specification { @Override InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { - throw new AbortExecutionException([new ValidationError(ValidationErrorType.UnknownType)]) // Retain as there is no alternative constructor for ValidationError + throw new AbortExecutionException([ValidationError.newValidationError().validationErrorType(ValidationErrorType.UnknownType).build()]) } } diff --git a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentEntryTest.groovy b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentEntryTest.groovy index 96e5d0f2f3..4cfa7d0e15 100644 --- a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentEntryTest.groovy +++ b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentEntryTest.groovy @@ -33,7 +33,7 @@ class PreparsedDocumentEntryTest extends Specification { def "Ensure a non-null errors returns"() { given: def errors = [new InvalidSyntaxError(new SourceLocation(0, 0), "bang"), - new ValidationError(ValidationErrorType.InvalidSyntax)] + ValidationError.newValidationError().validationErrorType(ValidationErrorType.InvalidSyntax).build()] when: def docEntry = new PreparsedDocumentEntry(errors) diff --git a/src/test/groovy/graphql/language/SerialisationTest.groovy b/src/test/groovy/graphql/language/SerialisationTest.groovy index 9a78e90913..8bd4ae46af 100644 --- a/src/test/groovy/graphql/language/SerialisationTest.groovy +++ b/src/test/groovy/graphql/language/SerialisationTest.groovy @@ -112,7 +112,11 @@ class SerialisationTest extends Specification { when: GraphQLError syntaxError1 = new InvalidSyntaxError(srcLoc(1, 1), "Bad Syntax 1") - GraphQLError validationError2 = new ValidationError(ValidationErrorType.FieldUndefined, srcLoc(2, 2), "Bad Query 2") + GraphQLError validationError2 = ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.FieldUndefined) + .sourceLocation(srcLoc(2, 2)) + .description("Bad Query 2") + .build() def originalEntry = new PreparsedDocumentEntry([syntaxError1, validationError2]) PreparsedDocumentEntry newEntry = serialisedDownAndBack(originalEntry) @@ -146,7 +150,11 @@ class SerialisationTest extends Specification { Document originalDoc = TestUtil.parseQuery(query) GraphQLError syntaxError1 = new InvalidSyntaxError(srcLoc(1, 1), "Bad Syntax 1") - GraphQLError validationError2 = new ValidationError(ValidationErrorType.FieldUndefined, srcLoc(2, 2), "Bad Query 2") + GraphQLError validationError2 = ValidationError.newValidationError() + .validationErrorType(ValidationErrorType.FieldUndefined) + .sourceLocation(srcLoc(2, 2)) + .description("Bad Query 2") + .build() def originalEntry = new PreparsedDocumentEntry(originalDoc, [syntaxError1, validationError2]) def originalAst = AstPrinter.printAst(originalEntry.getDocument()) PreparsedDocumentEntry newEntry = serialisedDownAndBack(originalEntry) From 39c40d2ef626689271a42a848272344589001b11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 16:45:28 +0000 Subject: [PATCH 063/393] Bump org.testng:testng from 7.8.0 to 7.9.0 Bumps [org.testng:testng](https://github.com/testng-team/testng) from 7.8.0 to 7.9.0. - [Release notes](https://github.com/testng-team/testng/releases) - [Changelog](https://github.com/testng-team/testng/blob/master/CHANGES.txt) - [Commits](https://github.com/testng-team/testng/compare/7.8.0...7.9.0) --- updated-dependencies: - dependency-name: org.testng:testng dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c44c4b751d..0c5d98c68e 100644 --- a/build.gradle +++ b/build.gradle @@ -120,7 +120,7 @@ dependencies { testImplementation 'org.reactivestreams:reactive-streams-tck:' + reactiveStreamsVersion testImplementation "io.reactivex.rxjava2:rxjava:2.2.21" - testImplementation 'org.testng:testng:7.8.0' // use for reactive streams test inheritance + testImplementation 'org.testng:testng:7.9.0' // use for reactive streams test inheritance testImplementation 'org.openjdk.jmh:jmh-core:1.37' testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' From 7aa8c23aae88646701a98ef30dde42894f029309 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 2 Jan 2024 14:47:34 +1100 Subject: [PATCH 064/393] WIP --- .../java/graphql/ExecutionResultImpl.java | 38 +++++----- .../InitialIncrementalExecutionResult.java | 12 ---- ...InitialIncrementalExecutionResultImpl.java | 70 ------------------- .../{defer => incremental}/DeferredItem.java | 2 +- .../DelayedIncrementalExecutionResult.java} | 4 +- ...elayedIncrementalExecutionResultImpl.java} | 16 ++--- .../IncrementalExecutionResult.java | 12 ++++ .../IncrementalExecutionResultImpl.java | 65 +++++++++++++++++ .../IncrementalItem.java | 7 +- .../{defer => incremental}/StreamedItem.java | 2 +- 10 files changed, 116 insertions(+), 112 deletions(-) delete mode 100644 src/main/java/graphql/defer/InitialIncrementalExecutionResult.java delete mode 100644 src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java rename src/main/java/graphql/{defer => incremental}/DeferredItem.java (98%) rename src/main/java/graphql/{defer/IncrementalExecutionResult.java => incremental/DelayedIncrementalExecutionResult.java} (67%) rename src/main/java/graphql/{defer/IncrementalExecutionResultImpl.java => incremental/DelayedIncrementalExecutionResultImpl.java} (60%) create mode 100644 src/main/java/graphql/incremental/IncrementalExecutionResult.java create mode 100644 src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java rename src/main/java/graphql/{defer => incremental}/IncrementalItem.java (95%) rename src/main/java/graphql/{defer => incremental}/StreamedItem.java (98%) diff --git a/src/main/java/graphql/ExecutionResultImpl.java b/src/main/java/graphql/ExecutionResultImpl.java index 33ddd67e21..62419a63a7 100644 --- a/src/main/java/graphql/ExecutionResultImpl.java +++ b/src/main/java/graphql/ExecutionResultImpl.java @@ -40,6 +40,10 @@ public ExecutionResultImpl(ExecutionResultImpl other) { this(other.dataPresent, other.data, other.errors, other.extensions); } + public > ExecutionResultImpl(Builder builder) { + this(builder.dataPresent, builder.data, builder.errors, builder.extensions); + } + private ExecutionResultImpl(boolean dataPresent, Object data, List errors, Map extensions) { this.dataPresent = dataPresent; this.data = data; @@ -103,61 +107,61 @@ public String toString() { '}'; } - public static Builder newExecutionResult() { - return new Builder(); + public static > Builder newExecutionResult() { + return new Builder<>(); } - public static class Builder implements ExecutionResult.Builder { + public static class Builder> implements ExecutionResult.Builder { private boolean dataPresent; private Object data; private List errors = new ArrayList<>(); private Map extensions; @Override - public Builder from(ExecutionResult executionResult) { + public T from(ExecutionResult executionResult) { dataPresent = executionResult.isDataPresent(); data = executionResult.getData(); errors = new ArrayList<>(executionResult.getErrors()); extensions = executionResult.getExtensions(); - return this; + return (T) this; } @Override - public Builder data(Object data) { + public T data(Object data) { dataPresent = true; this.data = data; - return this; + return (T) this; } @Override - public Builder errors(List errors) { + public T errors(List errors) { this.errors = errors; - return this; + return (T) this; } @Override - public Builder addErrors(List errors) { + public T addErrors(List errors) { this.errors.addAll(errors); - return this; + return (T) this; } @Override - public Builder addError(GraphQLError error) { + public T addError(GraphQLError error) { this.errors.add(error); - return this; + return (T) this; } @Override - public Builder extensions(Map extensions) { + public T extensions(Map extensions) { this.extensions = extensions; - return this; + return (T) this; } @Override - public Builder addExtension(String key, Object value) { + public T addExtension(String key, Object value) { this.extensions = (this.extensions == null ? new LinkedHashMap<>() : this.extensions); this.extensions.put(key, value); - return this; + return (T) this; } @Override diff --git a/src/main/java/graphql/defer/InitialIncrementalExecutionResult.java b/src/main/java/graphql/defer/InitialIncrementalExecutionResult.java deleted file mode 100644 index f38910e9b3..0000000000 --- a/src/main/java/graphql/defer/InitialIncrementalExecutionResult.java +++ /dev/null @@ -1,12 +0,0 @@ -package graphql.defer; - -import graphql.ExecutionResult; -import graphql.ExperimentalApi; -import org.reactivestreams.Publisher; - -@ExperimentalApi -public interface InitialIncrementalExecutionResult extends ExecutionResult { - boolean hasNext(); - - Publisher getIncrementalItemPublisher(); -} diff --git a/src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java b/src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java deleted file mode 100644 index e90420c6b3..0000000000 --- a/src/main/java/graphql/defer/InitialIncrementalExecutionResultImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -package graphql.defer; - -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import org.reactivestreams.Publisher; - -import java.util.LinkedHashMap; -import java.util.Map; - -public class InitialIncrementalExecutionResultImpl extends ExecutionResultImpl implements InitialIncrementalExecutionResult { - private final boolean hasNext; - private final Publisher incrementalItemPublisher; - - private InitialIncrementalExecutionResultImpl( - boolean hasNext, - Publisher incrementalItemPublisher, - ExecutionResultImpl other - ) { - super(other); - this.hasNext = hasNext; - this.incrementalItemPublisher = incrementalItemPublisher; - } - - @Override - public boolean hasNext() { - return this.hasNext; - } - - @Override - public Publisher getIncrementalItemPublisher() { - return incrementalItemPublisher; - } - - public static Builder newInitialIncrementalExecutionResult() { - return new Builder(); - } - - @Override - public Map toSpecification() { - Map map = new LinkedHashMap<>(super.toSpecification()); - map.put("hasNext", hasNext); - return map; - } - - public static class Builder { - private boolean hasNext = true; - private Publisher incrementalItemPublisher; - private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); - - public Builder hasNext(boolean hasNext) { - this.hasNext = hasNext; - return this; - } - - public Builder incrementalItemPublisher(Publisher incrementalItemPublisher) { - this.incrementalItemPublisher = incrementalItemPublisher; - return this; - } - - public Builder from(ExecutionResult executionResult) { - builder.from(executionResult); - return this; - } - - public InitialIncrementalExecutionResult build() { - ExecutionResultImpl build = (ExecutionResultImpl) builder.build(); - return new InitialIncrementalExecutionResultImpl(this.hasNext, this.incrementalItemPublisher, build); - } - } -} diff --git a/src/main/java/graphql/defer/DeferredItem.java b/src/main/java/graphql/incremental/DeferredItem.java similarity index 98% rename from src/main/java/graphql/defer/DeferredItem.java rename to src/main/java/graphql/incremental/DeferredItem.java index 248977d3c9..fcdbb65a9f 100644 --- a/src/main/java/graphql/defer/DeferredItem.java +++ b/src/main/java/graphql/incremental/DeferredItem.java @@ -1,4 +1,4 @@ -package graphql.defer; +package graphql.incremental; import graphql.ExperimentalApi; diff --git a/src/main/java/graphql/defer/IncrementalExecutionResult.java b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java similarity index 67% rename from src/main/java/graphql/defer/IncrementalExecutionResult.java rename to src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java index 7590c2b978..03650be1b2 100644 --- a/src/main/java/graphql/defer/IncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java @@ -1,11 +1,11 @@ -package graphql.defer; +package graphql.incremental; import graphql.ExperimentalApi; import java.util.List; @ExperimentalApi -public interface IncrementalExecutionResult { +public interface DelayedIncrementalExecutionResult { List getIncremental(); String getLabel(); diff --git a/src/main/java/graphql/defer/IncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java similarity index 60% rename from src/main/java/graphql/defer/IncrementalExecutionResultImpl.java rename to src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java index 1b66345a28..54645b24f0 100644 --- a/src/main/java/graphql/defer/IncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java @@ -1,14 +1,14 @@ -package graphql.defer; +package graphql.incremental; import java.util.Collections; import java.util.List; -public class IncrementalExecutionResultImpl implements IncrementalExecutionResult { +public class DelayedIncrementalExecutionResultImpl implements DelayedIncrementalExecutionResult { private final List incrementalItems; private final String label; private final boolean hasNext; - private IncrementalExecutionResultImpl(List incrementalItems, String label, boolean hasNext) { + private DelayedIncrementalExecutionResultImpl(List incrementalItems, String label, boolean hasNext) { this.incrementalItems = incrementalItems; this.label = label; this.hasNext = hasNext; @@ -38,23 +38,23 @@ public static class Builder { private List incrementalItems = Collections.emptyList(); private String label = null; - public IncrementalExecutionResultImpl.Builder hasNext(boolean hasNext) { + public DelayedIncrementalExecutionResultImpl.Builder hasNext(boolean hasNext) { this.hasNext = hasNext; return this; } - public IncrementalExecutionResultImpl.Builder incrementalItems(List incrementalItems) { + public DelayedIncrementalExecutionResultImpl.Builder incrementalItems(List incrementalItems) { this.incrementalItems = incrementalItems; return this; } - public IncrementalExecutionResultImpl.Builder label(String label) { + public DelayedIncrementalExecutionResultImpl.Builder label(String label) { this.label = label; return this; } - public IncrementalExecutionResultImpl build() { - return new IncrementalExecutionResultImpl(this.incrementalItems, this.label, this.hasNext); + public DelayedIncrementalExecutionResultImpl build() { + return new DelayedIncrementalExecutionResultImpl(this.incrementalItems, this.label, this.hasNext); } } } diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResult.java b/src/main/java/graphql/incremental/IncrementalExecutionResult.java new file mode 100644 index 0000000000..d0178092b7 --- /dev/null +++ b/src/main/java/graphql/incremental/IncrementalExecutionResult.java @@ -0,0 +1,12 @@ +package graphql.incremental; + +import graphql.ExecutionResult; +import graphql.ExperimentalApi; +import org.reactivestreams.Publisher; + +@ExperimentalApi +public interface IncrementalExecutionResult extends ExecutionResult { + boolean hasNext(); + + Publisher getIncrementalItemPublisher(); +} diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java new file mode 100644 index 0000000000..c12b4d40d4 --- /dev/null +++ b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java @@ -0,0 +1,65 @@ +package graphql.incremental; + +import graphql.ExecutionResultImpl; +import org.reactivestreams.Publisher; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class IncrementalExecutionResultImpl extends ExecutionResultImpl implements IncrementalExecutionResult { + private final boolean hasNext; + private final Publisher incrementalItemPublisher; + + private IncrementalExecutionResultImpl( + Builder builder + ) { + super(builder); + this.hasNext = builder.hasNext; + this.incrementalItemPublisher = builder.incrementalItemPublisher; + } + + @Override + public boolean hasNext() { + return this.hasNext; + } + + @Override + public Publisher getIncrementalItemPublisher() { + return incrementalItemPublisher; + } + + public static Builder newIncrementalExecutionResult() { + return new Builder(); + } + + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + map.put("hasNext", hasNext); + return map; + } + + public static class Builder extends ExecutionResultImpl.Builder { + private boolean hasNext = true; + private Publisher incrementalItemPublisher; + + public Builder hasNext(boolean hasNext) { + this.hasNext = hasNext; + return this; + } + + public Builder incrementalItemPublisher(Publisher incrementalItemPublisher) { + this.incrementalItemPublisher = incrementalItemPublisher; + return this; + } + +// public Builder from(ExecutionResult executionResult) { +// builder.from(executionResult); +// return this; +// } + + public IncrementalExecutionResult build() { + return new IncrementalExecutionResultImpl(this); + } + } +} diff --git a/src/main/java/graphql/defer/IncrementalItem.java b/src/main/java/graphql/incremental/IncrementalItem.java similarity index 95% rename from src/main/java/graphql/defer/IncrementalItem.java rename to src/main/java/graphql/incremental/IncrementalItem.java index 9f315916d2..9639c168e8 100644 --- a/src/main/java/graphql/defer/IncrementalItem.java +++ b/src/main/java/graphql/incremental/IncrementalItem.java @@ -1,4 +1,4 @@ -package graphql.defer; +package graphql.incremental; import graphql.ExperimentalApi; import graphql.GraphQLError; @@ -80,6 +80,11 @@ public IncrementalItem.Builder path(ResultPath path) { return this; } + public IncrementalItem.Builder path(List path) { + this.path = path; + return this; + } + public IncrementalItem.Builder errors(List errors) { this.errors = errors; return this; diff --git a/src/main/java/graphql/defer/StreamedItem.java b/src/main/java/graphql/incremental/StreamedItem.java similarity index 98% rename from src/main/java/graphql/defer/StreamedItem.java rename to src/main/java/graphql/incremental/StreamedItem.java index e8adfd254a..b07b259f8b 100644 --- a/src/main/java/graphql/defer/StreamedItem.java +++ b/src/main/java/graphql/incremental/StreamedItem.java @@ -1,4 +1,4 @@ -package graphql.defer; +package graphql.incremental; import graphql.ExperimentalApi; From e979b771266c312e1fa07186a96244d92fc93b4b Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 2 Jan 2024 14:53:44 +1100 Subject: [PATCH 065/393] Apply code formatting --- .../normalized/ExecutableNormalizedField.java | 9 +++++++++ .../ExecutableNormalizedOperationFactory.java | 14 ++++++++++++++ ...ExecutableNormalizedOperationToAstCompiler.java | 14 ++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 01f0d3fddd..78b4c6d8e4 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -128,6 +128,7 @@ private ExecutableNormalizedField(Builder builder) { * NOT {@code Cat} or {@code Dog} as their respective implementations would say. * * @param schema - the graphql schema in play + * * @return true if the field is conditional */ public boolean isConditional(@NotNull GraphQLSchema schema) { @@ -260,6 +261,7 @@ public void clearChildren() { * WARNING: This is not always the key in the execution result, because of possible field aliases. * * @return the name of this {@link ExecutableNormalizedField} + * * @see #getResultKey() * @see #getAlias() */ @@ -269,6 +271,7 @@ public String getName() { /** * @return the same value as {@link #getName()} + * * @see #getResultKey() * @see #getAlias() */ @@ -281,6 +284,7 @@ public String getFieldName() { * This is either a field alias or the value of {@link #getName()} * * @return the result key for this {@link ExecutableNormalizedField}. + * * @see #getName() */ public String getResultKey() { @@ -292,6 +296,7 @@ public String getResultKey() { /** * @return the field alias used or null if there is none + * * @see #getResultKey() * @see #getName() */ @@ -310,6 +315,7 @@ public ImmutableList getAstArguments() { * Returns an argument value as a {@link NormalizedInputValue} which contains its type name and its current value * * @param name the name of the argument + * * @return an argument value */ public NormalizedInputValue getNormalizedArgument(String name) { @@ -407,6 +413,7 @@ public List getChildren() { * Returns the list of child fields that would have the same result key * * @param resultKey the result key to check + * * @return a list of all direct {@link ExecutableNormalizedField} children with the specified result key */ public List getChildrenWithSameResultKey(String resultKey) { @@ -427,6 +434,7 @@ public List getChildren(int includingRelativeLevel) { * This returns the child fields that can be used if the object is of the specified object type * * @param objectTypeName the object type + * * @return a list of child fields that would apply to that object type */ public List getChildren(String objectTypeName) { @@ -559,6 +567,7 @@ public static Builder newNormalizedField() { * Allows this {@link ExecutableNormalizedField} to be transformed via a {@link Builder} consumer callback * * @param builderConsumer the consumer given a builder + * * @return a new transformed {@link ExecutableNormalizedField} */ public ExecutableNormalizedField transform(Consumer builderConsumer) { diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index c70e5de517..2f4a6eb8d1 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -105,6 +105,7 @@ public static Options defaultOptions() { * e.g. can be passed to {@link graphql.schema.Coercing} for parsing. * * @param locale the locale to use + * * @return new options object to use */ public Options locale(Locale locale) { @@ -117,6 +118,7 @@ public Options locale(Locale locale) { * Can be used to intercept input values e.g. using {@link graphql.execution.values.InputInterceptor}. * * @param graphQLContext the context to use + * * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { @@ -128,6 +130,7 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * against malicious operations. * * @param maxChildrenDepth the max depth + * * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { @@ -138,6 +141,7 @@ public Options maxChildrenDepth(int maxChildrenDepth) { * Controls whether defer execution is supported when creating instances of {@link ExecutableNormalizedOperation}. * * @param deferSupport true to enable support for defer + * * @return new options object to use */ @ExperimentalApi @@ -147,6 +151,7 @@ public Options deferSupport(boolean deferSupport) { /** * @return context to use during operation parsing + * * @see #graphQLContext(GraphQLContext) */ public GraphQLContext getGraphQLContext() { @@ -155,6 +160,7 @@ public GraphQLContext getGraphQLContext() { /** * @return locale to use during operation parsing + * * @see #locale(Locale) */ public Locale getLocale() { @@ -163,6 +169,7 @@ public Locale getLocale() { /** * @return maximum children depth before aborting parsing + * * @see #maxChildrenDepth(int) */ public int getMaxChildrenDepth() { @@ -171,6 +178,7 @@ public int getMaxChildrenDepth() { /** * @return whether support for defer is enabled + * * @see #deferSupport(boolean) */ @ExperimentalApi @@ -190,6 +198,7 @@ public boolean getDeferSupport() { * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param coercedVariableValues the coerced variables to use + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( @@ -215,6 +224,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param operationName the operation name to use * @param coercedVariableValues the coerced variables to use * @param options the {@link Options} to use for parsing + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( @@ -241,6 +251,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param operationDefinition the operation to be executed * @param fragments a set of fragments associated with the operation * @param coercedVariableValues the coerced variables to use + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation(GraphQLSchema graphQLSchema, @@ -263,6 +274,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param rawVariables the raw variables to be coerced + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, @@ -287,6 +299,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param rawVariables the raw variables that have not yet been coerced * @param locale the {@link Locale} to use during coercion * @param graphQLContext the {@link GraphQLContext} to use during coercion + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables( @@ -315,6 +328,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param operationName the operation name to use * @param rawVariables the raw variables that have not yet been coerced * @param options the {@link Options} to use for parsing + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 6d35061965..f65ab4a6f5 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -97,6 +97,7 @@ public Map getVariables() { * @param operationName the name of the operation to use * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -119,6 +120,7 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -143,7 +145,9 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work + * * @return a {@link CompilerResult} object + * * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, VariablePredicate) */ @ExperimentalApi @@ -169,7 +173,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work + * * @return a {@link CompilerResult} object + * * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, Map, VariablePredicate) */ @ExperimentalApi @@ -498,8 +504,12 @@ public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } ExecutionFragmentDetails that = (ExecutionFragmentDetails) o; return Objects.equals(typeName, that.typeName) && Objects.equals(deferExecution, that.deferExecution); } From 202573fd6ac6fb4dadd6cd7579c25d5a50d06472 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 2 Jan 2024 18:14:29 +1100 Subject: [PATCH 066/393] Fix edge case --- .../ExecutableNormalizedOperationFactory.java | 61 ++++++++++- ...tableNormalizedOperationToAstCompiler.java | 9 +- ...NormalizedOperationFactoryDeferTest.groovy | 101 +++++++++++++++++- ...izedOperationToAstCompilerDeferTest.groovy | 9 -- 4 files changed, 161 insertions(+), 19 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 2f4a6eb8d1..62c5825292 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -27,6 +27,7 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; +import graphql.language.TypeName; import graphql.language.VariableDefinition; import graphql.normalized.incremental.DeferExecution; import graphql.normalized.incremental.IncrementalNodes; @@ -42,19 +43,24 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; @@ -66,6 +72,7 @@ import static graphql.util.FpKit.intersection; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toSet; /** @@ -712,8 +719,9 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build(); - + Set deferExecutions = deferExecutionsBuilder.build().stream() + .filter(distinctByNullLabelAndType()) + .collect(toCollection(LinkedHashSet::new)); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -726,14 +734,61 @@ private List groupByCommonParentsWithDeferSupport(Collectio if (groupByAstParent.size() == 1) { return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecutions)); } + ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecutions)); + + Set filteredDeferExecutions = deferExecutions.stream() + .filter(deferExecution -> deferExecution.getTargetType() == null || + deferExecution.getTargetType().getName().equals(objectType.getName())) + .collect(toCollection(LinkedHashSet::new)); + + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); } return result.build(); } + /** + * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. + * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. + * For example, this query: + *
+     *     query example {
+     *        ... @defer {
+     *            name
+     *        }
+     *        ... @defer {
+     *            name
+     *        }
+     *     }
+     * 
+ * should result on single ENF. Essentially: + *
+     *     query example {
+     *        ... @defer {
+     *            name
+     *        }
+     *     }
+     * 
+ */ + private static @NotNull Predicate distinctByNullLabelAndType() { + Map seen = new ConcurrentHashMap<>(); + + return deferExecution -> { + if (deferExecution.getLabel() == null) { + String typeName = Optional.ofNullable(deferExecution.getTargetType()) + .map(TypeName::getName) + .map(String::toUpperCase) + .orElse("null"); + + return seen.putIfAbsent(typeName, Boolean.TRUE) == null; + } + + return true; + }; + } + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() .map(DeferExecution::getLabel) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index f65ab4a6f5..861ded72f5 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -291,12 +291,9 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor .add(field); } else { deferExecutions.forEach(deferExecution -> { - if (deferExecution.getTargetType() == null || - objectTypeName.equals(deferExecution.getTargetType().getName())) { - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) - .add(field); - } + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) + .add(field); }); } }); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 9c184e5966..2fad250b50 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -32,9 +32,21 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { owner: Person } + type Cat implements Animal { + name: String + breed: String + color: String + siblings: [Cat] + } + + type Fish implements Animal { + name: String + } + type Person { firstname: String lastname: String + bestFriend: Person } """ @@ -66,6 +78,64 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "fragments on non-conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + name + } + ... on Dog @defer { + name + } + ... on Animal @defer { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer[null]', + ] + } + + def "fragments on conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + breed + } + ... on Dog @defer { + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + 'Cat.breed defer[null]', + 'Dog.breed defer[null]' + ] + } + def "defer on a single field via inline fragment with type"() { given: @@ -233,6 +303,35 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "multiple fields and multiple defers - no label"() { + given: + + String query = ''' + query q { + dog { + ... @defer { + name + } + + ... @defer { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[null]', + ] + } + def "multiple fields and a multiple defers with same label are not allowed"() { given: @@ -354,7 +453,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - 'Dog.name', + '[Cat, Dog, Fish].name', 'Dog.owner defer[dog-defer]', 'Person.firstname', 'Person.lastname defer[lastname-defer]', diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index d2c88489b6..090fc26413 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -204,9 +204,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { breed } - ... @defer { - breed - } } } ''' @@ -307,9 +304,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { name } - ... @defer { - name - } } } ''' @@ -454,9 +448,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { - ... @defer { - name - } ... @defer { name } From 4db220e19eb73badb23bb8b710d0b1f011c6364f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 2 Jan 2024 18:27:50 +1100 Subject: [PATCH 067/393] Replace Guava's Multimap with Map in public APIs --- .../ExecutableNormalizedOperation.java | 5 +++-- ...tableNormalizedOperationToAstCompiler.java | 22 +++++++++---------- ...NormalizedOperationFactoryDeferTest.groovy | 2 +- ...ormalizedOperationToAstCompilerTest.groovy | 4 ++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index 9bfef00b26..9440620c49 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -13,6 +13,7 @@ import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLFieldsContainer; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -135,8 +136,8 @@ public Map getNormalizedFieldToQuery * @return a map of {@link ExecutableNormalizedField} to its {@link DeferExecution} */ @ExperimentalApi - public ImmutableListMultimap getNormalizedFieldToDeferExecution() { - return normalizedFieldToDeferExecution; + public Map> getNormalizedFieldToDeferExecution() { + return normalizedFieldToDeferExecution.asMap(); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 861ded72f5..b4ce04db18 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -1,7 +1,6 @@ package graphql.normalized; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import graphql.Assert; import graphql.Directives; @@ -34,6 +33,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -156,7 +156,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @Nullable VariablePredicate variablePredicate, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); } @@ -185,7 +185,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); } @@ -195,7 +195,7 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); @@ -222,7 +222,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { if (normalizedFieldToDeferExecution != null) { return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); } else { @@ -270,7 +270,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - @NotNull ImmutableListMultimap normalizedFieldToDeferExecution, + @NotNull Map> normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -280,12 +280,12 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - List deferExecutions = normalizedFieldToDeferExecution.get(nf); + Collection deferExecutions = normalizedFieldToDeferExecution.get(nf); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution) .forEach((objectTypeName, field) -> { - if (deferExecutions.isEmpty()) { + if (deferExecutions == null || deferExecutions.isEmpty()) { fieldsByFragmentDetails .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) .add(field); @@ -298,7 +298,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor } }); - } else if (!deferExecutions.isEmpty()) { + } else if (deferExecutions != null && !deferExecutions.isEmpty()) { Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); deferExecutions.forEach(deferExecution -> { @@ -344,7 +344,7 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { @@ -362,7 +362,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 2fad250b50..a8b96e0fc8 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -606,7 +606,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { String printDeferExecutionDetails(ExecutableNormalizedField field) { def deferExecutions = normalizedFieldToDeferExecution.get(field) - if (deferExecutions.isEmpty()) { + if (deferExecutions == null || deferExecutions.isEmpty()) { return "" } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index e0720dca3c..99079c8705 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2184,7 +2184,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat String operationName, List topLevelFields, VariablePredicate variablePredicate, - ImmutableListMultimap normalizedFieldToDeferExecution + Map> normalizedFieldToDeferExecution ) { return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution) } @@ -2196,7 +2196,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat List topLevelFields, Map normalizedFieldToQueryDirectives, VariablePredicate variablePredicate, - ImmutableListMultimap normalizedFieldToDeferExecution + Map> normalizedFieldToDeferExecution ) { if (deferSupport) { return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution) From 6446ad484950b41d461835a9f7cdd2d4f2040e87 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 5 Jan 2024 14:07:28 +1100 Subject: [PATCH 068/393] Big refactoring: - introduce NormalizedDeferExecution - add defer execution state to ENF --- .../java/graphql/normalized/ENFMerger.java | 22 +- .../normalized/ExecutableNormalizedField.java | 27 ++ .../ExecutableNormalizedOperation.java | 17 +- .../ExecutableNormalizedOperationFactory.java | 83 ++-- ...tableNormalizedOperationToAstCompiler.java | 69 ++-- .../incremental/DeferDeclaration.java | 35 ++ .../incremental/DeferExecution.java | 36 -- .../incremental/IncrementalNodes.java | 8 +- .../incremental/NormalizedDeferExecution.java | 43 +++ .../NormalizedDeferExecutionFactory.java | 119 ++++++ ...NormalizedOperationFactoryDeferTest.groovy | 363 ++++++++++++++++-- ...izedOperationToAstCompilerDeferTest.groovy | 51 +-- ...ormalizedOperationToAstCompilerTest.groovy | 44 +-- 13 files changed, 683 insertions(+), 234 deletions(-) create mode 100644 src/main/java/graphql/normalized/incremental/DeferDeclaration.java delete mode 100644 src/main/java/graphql/normalized/incremental/DeferExecution.java create mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java create mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index 97d182a5f4..d876cf56db 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,9 +1,11 @@ package graphql.normalized; +import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; +import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -19,7 +21,12 @@ @Internal public class ENFMerger { - public static void merge(ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema) { + public static void merge( + ExecutableNormalizedField parent, + List childrenWithSameResultKey, + GraphQLSchema schema, + Multimap normalizedFieldToDeferExecution + ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same List> possibleGroupsToMerge = new ArrayList<>(); @@ -28,7 +35,7 @@ public static void merge(ExecutableNormalizedField parent, List group : possibleGroupsToMerge) { for (ExecutableNormalizedField fieldInGroup : group) { - if(field.getFieldName().equals(Introspection.TypeNameMetaFieldDef.getName())) { + if (field.getFieldName().equals(Introspection.TypeNameMetaFieldDef.getName())) { addToGroup = true; group.add(field); continue overPossibleGroups; @@ -58,13 +65,18 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) } boolean mergeable = areFieldSetsTheSame(listOfChildrenForGroup); if (mergeable) { - Set mergedObjects = new LinkedHashSet<>(); - groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); // patching the first one to contain more objects, remove all others Iterator iterator = groupOfFields.iterator(); ExecutableNormalizedField first = iterator.next(); + + Set mergedObjects = new LinkedHashSet<>(); + groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); while (iterator.hasNext()) { - parent.getChildren().remove(iterator.next()); + ExecutableNormalizedField next = iterator.next(); + // Move defer executions from removed field into the merged field's entry + normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); + parent.getChildren().remove(next); + normalizedFieldToDeferExecution.removeAll(next); } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 78b4c6d8e4..93f128fa59 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -2,13 +2,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.Internal; import graphql.Mutable; import graphql.PublicApi; import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; +import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -63,6 +66,9 @@ public class ExecutableNormalizedField { private final String fieldName; private final int level; + // Mutable List on purpose: it is modified after creation + private final LinkedHashSet deferExecutions; + private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; this.resolvedArguments = builder.resolvedArguments; @@ -73,6 +79,7 @@ private ExecutableNormalizedField(Builder builder) { this.children = builder.children; this.level = builder.level; this.parent = builder.parent; + this.deferExecutions = builder.deferExecutions; } /** @@ -255,6 +262,12 @@ public void clearChildren() { this.children.clear(); } + @Internal + public void setDeferExecutions(Collection deferExecutions) { + this.deferExecutions.clear(); + this.deferExecutions.addAll(deferExecutions); + } + /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -460,6 +473,12 @@ public ExecutableNormalizedField getParent() { return parent; } + // TODO: Javadoc + @ExperimentalApi + public LinkedHashSet getDeferExecutions() { + return deferExecutions; + } + @Internal public void replaceParent(ExecutableNormalizedField newParent) { this.parent = newParent; @@ -587,6 +606,8 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private Builder() { } @@ -600,6 +621,7 @@ private Builder(ExecutableNormalizedField existing) { this.children = new ArrayList<>(existing.children); this.level = existing.getLevel(); this.parent = existing.getParent(); + this.deferExecutions = existing.getDeferExecutions(); } public Builder clearObjectTypesNames() { @@ -655,6 +677,11 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } + public Builder deferExecutions(LinkedHashSet deferExecutions) { + this.deferExecutions = deferExecutions; + return this; + } + public ExecutableNormalizedField build() { return new ExecutableNormalizedField(this); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index 9440620c49..ce50c9931b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -2,18 +2,15 @@ import com.google.common.collect.ImmutableListMultimap; import graphql.Assert; -import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.MergedField; import graphql.execution.ResultPath; import graphql.execution.directives.QueryDirectives; import graphql.language.Field; import graphql.language.OperationDefinition; -import graphql.normalized.incremental.DeferExecution; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLFieldsContainer; -import java.util.Collection; import java.util.List; import java.util.Map; @@ -33,7 +30,6 @@ public class ExecutableNormalizedOperation { private final ImmutableListMultimap fieldToNormalizedField; private final Map normalizedFieldToMergedField; private final Map normalizedFieldToQueryDirectives; - private final ImmutableListMultimap normalizedFieldToDeferExecution; private final ImmutableListMultimap coordinatesToNormalizedFields; public ExecutableNormalizedOperation( @@ -43,8 +39,7 @@ public ExecutableNormalizedOperation( ImmutableListMultimap fieldToNormalizedField, Map normalizedFieldToMergedField, Map normalizedFieldToQueryDirectives, - ImmutableListMultimap coordinatesToNormalizedFields, - ImmutableListMultimap normalizedFieldToDeferExecution + ImmutableListMultimap coordinatesToNormalizedFields ) { this.operation = operation; this.operationName = operationName; @@ -53,7 +48,6 @@ public ExecutableNormalizedOperation( this.normalizedFieldToMergedField = normalizedFieldToMergedField; this.normalizedFieldToQueryDirectives = normalizedFieldToQueryDirectives; this.coordinatesToNormalizedFields = coordinatesToNormalizedFields; - this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } /** @@ -132,15 +126,6 @@ public Map getNormalizedFieldToQuery } - /** - * @return a map of {@link ExecutableNormalizedField} to its {@link DeferExecution} - */ - @ExperimentalApi - public Map> getNormalizedFieldToDeferExecution() { - return normalizedFieldToDeferExecution.asMap(); - } - - /** * This looks up the {@link QueryDirectives} associated with the given {@link ExecutableNormalizedField} * diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 62c5825292..0ec0891ab8 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,8 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.LinkedHashMultimap; import graphql.Assert; import graphql.ExperimentalApi; import graphql.GraphQLContext; @@ -27,10 +29,10 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; -import graphql.language.TypeName; import graphql.language.VariableDefinition; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.DeferDeclaration; import graphql.normalized.incremental.IncrementalNodes; +import graphql.normalized.incremental.NormalizedDeferExecutionFactory; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -410,7 +412,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; - ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); Consumer captureCollectNFResult = (collectNFResult -> { @@ -440,8 +442,13 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); } + + if (options.deferSupport) { + NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); + } + return new ExecutableNormalizedOperation( operationDefinition.getOperation(), operationDefinition.getName(), @@ -449,8 +456,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr fieldToNormalizedField.build(), normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), - coordinatesToNormalizedFields.build(), - normalizedFieldToDeferExecution.build() + coordinatesToNormalizedFields.build() ); } @@ -529,9 +535,13 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; - private final ImmutableListMultimap normalizedFieldToDeferExecution; + private final ImmutableSetMultimap normalizedFieldToDeferExecution; - public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields, ImmutableListMultimap normalizedFieldToDeferExecution) { + public CollectNFResult( + Collection children, + ImmutableListMultimap normalizedFieldToAstFields, + ImmutableSetMultimap normalizedFieldToDeferExecution + ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; @@ -547,7 +557,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableListMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -568,7 +578,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); @@ -594,7 +604,7 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); @@ -613,7 +623,7 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, - ImmutableListMultimap.Builder normalizedFieldToDeferExecution, + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -669,9 +679,9 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p private static class CollectedFieldGroup { Set objectTypes; Set fields; - Set deferExecutions; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; this.deferExecutions = deferExecutions; @@ -706,12 +716,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferExecution collectedDeferExecution = collectedField.deferExecution; + DeferDeclaration collectedDeferExecution = collectedField.deferExecution; if (collectedDeferExecution != null) { deferExecutionsBuilder.add(collectedDeferExecution); @@ -719,7 +729,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build().stream() + Set deferExecutions = deferExecutionsBuilder.build().stream() .filter(distinctByNullLabelAndType()) .collect(toCollection(LinkedHashSet::new)); @@ -739,9 +749,8 @@ private List groupByCommonParentsWithDeferSupport(Collectio for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - Set filteredDeferExecutions = deferExecutions.stream() - .filter(deferExecution -> deferExecution.getTargetType() == null || - deferExecution.getTargetType().getName().equals(objectType.getName())) + Set filteredDeferExecutions = deferExecutions.stream() + .filter(filter(objectType)) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -749,6 +758,21 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } + private static Predicate filter(GraphQLObjectType objectType) { + return deferExecution -> { + if (deferExecution.getTargetType() == null) { + return true; + } + + if (deferExecution.getTargetType().equals(objectType.getName())) { + return true; + } + + return objectType.getInterfaces().stream() + .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); + }; + } + /** * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. @@ -772,13 +796,12 @@ private List groupByCommonParentsWithDeferSupport(Collectio * } * */ - private static @NotNull Predicate distinctByNullLabelAndType() { + private static @NotNull Predicate distinctByNullLabelAndType() { Map seen = new ConcurrentHashMap<>(); return deferExecution -> { if (deferExecution.getLabel() == null) { String typeName = Optional.ofNullable(deferExecution.getTargetType()) - .map(TypeName::getName) .map(String::toUpperCase) .orElse("null"); @@ -789,9 +812,9 @@ private List groupByCommonParentsWithDeferSupport(Collectio }; } - private Set listDuplicatedLabels(Collection deferExecutions) { + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() - .map(DeferExecution::getLabel) + .map(DeferDeclaration::getLabel) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() @@ -806,7 +829,7 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferExecution deferExecution + DeferDeclaration deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { @@ -823,9 +846,9 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; - DeferExecution deferExecution; + DeferDeclaration deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferDeclaration deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; @@ -861,7 +884,7 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), fragmentSpread.getDirectives(), fragmentDefinition.getTypeCondition() @@ -890,7 +913,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), inlineFragment.getTypeCondition() @@ -904,7 +927,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferExecution deferExecution + DeferDeclaration deferExecution ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index b4ce04db18..cae6f42ae7 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,7 +23,8 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.DeferDeclaration; +import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -35,6 +36,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -129,7 +131,7 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, null); + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, false); } @@ -139,12 +141,11 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * - * @param schema the graphql schema to use - * @param operationKind the kind of operation - * @param operationName the name of the operation to use - * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from - * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * * @return a {@link CompilerResult} object * @@ -155,9 +156,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull OperationDefinition.Operation operationKind, @Nullable String operationName, @NotNull List topLevelFields, - @Nullable VariablePredicate variablePredicate, - @Nullable Map> normalizedFieldToDeferExecution) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); + @Nullable VariablePredicate variablePredicate + ) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); } /** @@ -172,7 +173,6 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work * * @return a {@link CompilerResult} object * @@ -184,9 +184,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, - @Nullable VariablePredicate variablePredicate, - @Nullable Map> normalizedFieldToDeferExecution) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); + @Nullable VariablePredicate variablePredicate + ) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, true); } private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -195,11 +195,11 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable Map> normalizedFieldToDeferExecution) { + boolean deferSupport) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); - List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); + List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport); SelectionSet selectionSet = new SelectionSet(selections); OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition() @@ -222,9 +222,9 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map> normalizedFieldToDeferExecution) { - if (normalizedFieldToDeferExecution != null) { - return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); + boolean deferSupport) { + if (deferSupport) { + return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); } else { return subselectionsForNormalizedFieldNoDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); } @@ -243,13 +243,13 @@ private static List> subselectionsForNormalizedFieldNoDeferSupport( for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, false) .forEach((objectTypeName, field) -> fieldsByTypeCondition .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, false)); } } @@ -270,7 +270,6 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - @NotNull Map> normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -280,10 +279,10 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - Collection deferExecutions = normalizedFieldToDeferExecution.get(nf); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) .forEach((objectTypeName, field) -> { if (deferExecutions == null || deferExecutions.isEmpty()) { fieldsByFragmentDetails @@ -299,7 +298,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor }); } else if (deferExecutions != null && !deferExecutions.isEmpty()) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true); deferExecutions.forEach(deferExecution -> { fieldsByFragmentDetails @@ -307,7 +306,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor .add(field); }); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true)); } } @@ -323,8 +322,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); + if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -344,11 +343,11 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map> normalizedFieldToDeferExecution) { + boolean deferSupport) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport)); } return groupedFields; @@ -362,7 +361,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map> normalizedFieldToDeferExecution) { + boolean deferSupport) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); @@ -376,7 +375,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, executableNormalizedField.getChildren(), normalizedFieldToQueryDirectives, variableAccumulator, - normalizedFieldToDeferExecution + deferSupport ); } @@ -492,9 +491,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferExecution deferExecution; + private final NormalizedDeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java new file mode 100644 index 0000000000..688b9dd8a8 --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java @@ -0,0 +1,35 @@ +package graphql.normalized.incremental; + +import graphql.Internal; + +import javax.annotation.Nullable; + +/** + * TODO: Javadoc + */ +@Internal +public class DeferDeclaration { + private final String label; + private final String targetType; + + public DeferDeclaration(@Nullable String label, @Nullable String targetType) { + this.label = label; + this.targetType = targetType; + } + + /** + * @return the label associated with this defer declaration + */ + @Nullable + public String getLabel() { + return label; + } + + /** + * @return the name of the type that is the target of the defer declaration + */ + @Nullable + public String getTargetType() { + return targetType; + } +} diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java deleted file mode 100644 index 8d62c0acaa..0000000000 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ /dev/null @@ -1,36 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.ExperimentalApi; -import graphql.language.TypeName; - -import javax.annotation.Nullable; - -/** - * Represents the defer execution aspect of a query field - */ -@ExperimentalApi -public class DeferExecution { - private final String label; - private final TypeName targetType; - - public DeferExecution(@Nullable String label, @Nullable TypeName targetType) { - this.label = label; - this.targetType = targetType; - } - - /** - * @return the label associated with this defer execution - */ - @Nullable - public String getLabel() { - return label; - } - - /** - * @return the {@link TypeName} of the type that is the target of the defer execution - */ - @Nullable - public TypeName getTargetType() { - return targetType; - } -} diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 9898616ddc..5b3f240d36 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -19,7 +19,7 @@ @Internal public class IncrementalNodes { - public DeferExecution getDeferExecution( + public DeferDeclaration getDeferExecution( Map variables, List directives, @Nullable TypeName targetType @@ -38,13 +38,15 @@ public DeferExecution getDeferExecution( Object label = argumentValues.get("label"); + String targetTypeName = targetType == null ? null : targetType.getName(); + if (label == null) { - return new DeferExecution(null, targetType); + return new DeferDeclaration(null, targetTypeName); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label, targetType); + return new DeferDeclaration((String) label, targetTypeName); } diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java new file mode 100644 index 0000000000..aad493018b --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java @@ -0,0 +1,43 @@ +package graphql.normalized.incremental; + +import graphql.ExperimentalApi; + +import javax.annotation.Nullable; +import java.util.Set; + +/** + * Represents the aspects of defer that are important for runtime execution. + */ +@ExperimentalApi +public class NormalizedDeferExecution { + private final DeferBlock deferBlock; + private final Set objectTypeNames; + + public NormalizedDeferExecution(DeferBlock deferBlock, Set objectTypeNames) { + this.deferBlock = deferBlock; + this.objectTypeNames = objectTypeNames; + } + + public Set getObjectTypeNames() { + return objectTypeNames; + } + + public DeferBlock getDeferBlock() { + return deferBlock; + } + + // TODO: Javadoc + @ExperimentalApi + public static class DeferBlock { + private final String label; + + public DeferBlock(@Nullable String label) { + this.label = label; + } + + @Nullable + public String getLabel() { + return label; + } + } +} diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java new file mode 100644 index 0000000000..0ef085332b --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -0,0 +1,119 @@ +package graphql.normalized.incremental; + +import com.google.common.collect.Multimap; +import graphql.Internal; +import graphql.normalized.ExecutableNormalizedField; +import graphql.normalized.incremental.NormalizedDeferExecution.DeferBlock; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +// TODO: Javadoc +@Internal +public class NormalizedDeferExecutionFactory { + public static void normalizeDeferExecutions( + GraphQLSchema graphQLSchema, + Multimap normalizedFieldDeferExecution + ) { + new DeferExecutionMergerInner(graphQLSchema, normalizedFieldDeferExecution).execute(); + } + + private static class DeferExecutionMergerInner { + private final GraphQLSchema graphQLSchema; + private final Multimap input; + + private DeferExecutionMergerInner( + GraphQLSchema graphQLSchema, + Multimap normalizedFieldToDeferExecution + ) { + this.graphQLSchema = graphQLSchema; + this.input = normalizedFieldToDeferExecution; + } + + private void execute() { + Map declarationToBlock = new HashMap<>(); + + this.input.keySet().forEach(field -> { + Collection executionsForField = input.get(field); + + Set fieldTypes = field.getObjectTypeNames().stream() + .map(graphQLSchema::getType) + .filter(GraphQLObjectType.class::isInstance) + .map(GraphQLObjectType.class::cast) + .collect(Collectors.toSet()); + + Set fieldTypeNames = fieldTypes.stream().map(GraphQLObjectType::getName).collect(Collectors.toSet()); + + Map, List> executionsByLabel = executionsForField.stream() + .collect(Collectors.groupingBy(execution -> Optional.ofNullable(execution.getLabel()))); + + Set deferExecutions = executionsByLabel.keySet().stream() + .map(label -> { + List executionsForLabel = executionsByLabel.get(label); + + DeferBlock deferBlock = executionsForLabel.stream() + .map(declarationToBlock::get) + .filter(Objects::nonNull) + .findFirst() + .orElse(new DeferBlock(label.orElse(null))); + + Set types = executionsForLabel.stream() + .map(deferExecution -> { + declarationToBlock.put(deferExecution, deferBlock); + return deferExecution.getTargetType(); + }) + .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) + .collect(Collectors.toSet()); + + return new NormalizedDeferExecution(deferBlock, types); + + }) + .collect(Collectors.toSet()); + + if (!deferExecutions.isEmpty()) { + // Mutate the field, by setting deferExecutions + field.setDeferExecutions(deferExecutions); + } + }); + } + + @NotNull + private Function> mapToPossibleTypes(Set fieldTypeNames, Set fieldTypes) { + return typeName -> { + if (typeName == null) { + return fieldTypeNames.stream(); + } + + GraphQLType type = graphQLSchema.getType(typeName); + + if (type instanceof GraphQLInterfaceType) { + return fieldTypes.stream() + .filter(filterImplementsInterface((GraphQLInterfaceType) type)) + .map(GraphQLObjectType::getName); + } + + return Stream.of(typeName); + }; + } + + private static Predicate filterImplementsInterface(GraphQLInterfaceType interfaceType) { + return objectType -> objectType.getInterfaces().stream() + .anyMatch(implementedInterface -> implementedInterface.getName().equals(interfaceType.getName())); + } + } + +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index a8b96e0fc8..eb2260ee11 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -20,27 +20,36 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { type Query { dog: Dog animal: Animal + mammal: Mammal } - interface Animal { + interface LivingThing { + age: Int + } + + interface Animal implements LivingThing { name: String + age: Int } - type Dog implements Animal { + type Dog implements Animal & LivingThing { name: String + age: Int breed: String owner: Person } - type Cat implements Animal { + type Cat implements Animal & LivingThing { name: String + age: Int breed: String color: String siblings: [Cat] } - type Fish implements Animal { + type Fish implements Animal & LivingThing { name: String + age: Int } type Person { @@ -48,6 +57,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { lastname: String bestFriend: Person } + + union Mammal = Dog | Cat """ GraphQLSchema graphQLSchema = TestUtil.schema(schema) @@ -74,7 +85,31 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', 'Dog.name', - 'Dog.breed defer[breed-defer]', + 'Dog.breed defer{[label=breed-defer;types=[Dog]]}', + ] + } + + def "fragment on interface field with no type"() { + given: + + String query = ''' + query q { + animal { + ... @defer { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}", ] } @@ -104,7 +139,171 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - '[Cat, Dog, Fish].name defer[null]', + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}", + ] + } + + def "fragments on subset of non-conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + name + } + ... on Dog @defer { + name + } + ... on Fish { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog]]}", + ] + } + + def "fragment on interface"() { + given: + + String query = ''' + query q { + animal { + ... on Animal @defer { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + ] + } + + def "fragment on distant interface"() { + given: + + String query = ''' + query q { + animal { + ... on LivingThing @defer { + age + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].age defer{[label=null;types=[Cat, Dog, Fish]]}', + ] + } + + def "fragment on union"() { + given: + + String query = ''' + query q { + mammal { + ... on Dog @defer { + name + breed + } + ... on Cat @defer { + name + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.mammal', + '[Dog, Cat].name defer{[label=null;types=[Cat, Dog]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}', + 'Cat.breed defer{[label=null;types=[Cat]]}', + ] + } + + def "fragments on interface"() { + given: + + String query = ''' + query q { + animal { + ... on Animal @defer { + name + } + ... on Animal @defer { + age + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + '[Cat, Dog, Fish].age defer{[label=null;types=[Cat, Dog, Fish]]}', + ] + } + + def "defer on a subselection of non-conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + name + } + ... on Dog { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog].name defer{[label=null;types=[Cat]]}', ] } @@ -131,8 +330,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - 'Cat.breed defer[null]', - 'Dog.breed defer[null]' + 'Cat.breed defer{[label=null;types=[Cat]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}' ] } @@ -158,18 +357,69 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', 'Dog.name', - 'Dog.breed defer[breed-defer]', + 'Dog.breed defer{[label=breed-defer;types=[Dog]]}', ] } - def "defer on 2 fields"() { + def "1 defer on 2 fields"() { + given: + String query = ''' + query q { + animal { + ... @defer { + name + } + + ... on Dog @defer { + name + breed + } + + ... on Cat @defer { + name + breed + } + } + } + ''' + + Map variables = [:] + + when: + def executableNormalizedOperation = createExecutableNormalizedOperations(query, variables); + + List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) + + then: "should result in the same instance of defer" + def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") + def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") + def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") + + nameField.deferExecutions.size() == 1 + dogBreedField.deferExecutions.size() == 1 + catBreedField.deferExecutions.size() == 1 + + // same label instances + nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock + dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock + + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}', + 'Cat.breed defer{[label=null;types=[Cat]]}', + ] + } + + def "2 defers on 2 fields"() { given: String query = ''' query q { dog { - ... @defer(label: "breed-defer") { + ... @defer{ name + } + ... @defer{ breed } } @@ -179,12 +429,23 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Map variables = [:] when: - List printedTree = executeQueryAndPrintTree(query, variables) + def executableNormalizedOperation = createExecutableNormalizedOperations(query, variables); + + List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) + + then: "should result in 2 different instances of defer" + def nameField = findField(executableNormalizedOperation, "Dog", "name") + def breedField = findField(executableNormalizedOperation, "Dog", "breed") + + nameField.deferExecutions.size() == 1 + breedField.deferExecutions.size() == 1 + + // different label instances + nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock - then: printedTree == ['Query.dog', - 'Dog.name defer[breed-defer]', - 'Dog.breed defer[breed-defer]', + 'Dog.name defer{[label=null;types=[Dog]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}', ] } @@ -211,8 +472,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[breed-defer]', - 'Dog.breed defer[breed-defer]', + 'Dog.name defer{[label=breed-defer;types=[Dog]]}', + 'Dog.breed defer{[label=breed-defer;types=[Dog]]}', ] } @@ -241,7 +502,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[name-defer,another-name-defer]', + 'Dog.name defer{[label=another-name-defer;types=[Dog]],[label=name-defer;types=[Dog]]}' ] } @@ -270,7 +531,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[name-defer]', + 'Dog.name defer{[label=name-defer;types=[Dog]]}', ] } @@ -299,7 +560,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[null]', + 'Dog.name defer{[label=null;types=[Dog]]}', ] } @@ -328,7 +589,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[null]', + 'Dog.name defer{[label=null;types=[Dog]]}', ] } @@ -387,10 +648,10 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[null]', - 'Dog.owner defer[null]', + 'Dog.name defer{[label=null;types=[Dog]]}', + 'Dog.owner defer{[label=null;types=[Dog]]}', 'Person.firstname', - 'Person.lastname defer[null]', + 'Person.lastname defer{[label=null;types=[Person]]}', ] } @@ -420,10 +681,10 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[dog-defer]', - 'Dog.owner defer[dog-defer]', + 'Dog.name defer{[label=dog-defer;types=[Dog]]}', + 'Dog.owner defer{[label=dog-defer;types=[Dog]]}', 'Person.firstname', - 'Person.lastname defer[lastname-defer]', + 'Person.lastname defer{[label=lastname-defer;types=[Person]]}', ] } @@ -454,9 +715,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', '[Cat, Dog, Fish].name', - 'Dog.owner defer[dog-defer]', + 'Dog.owner defer{[label=dog-defer;types=[Dog]]}', 'Person.firstname', - 'Person.lastname defer[lastname-defer]', + 'Person.lastname defer{[label=lastname-defer;types=[Person]]}', ] } @@ -484,7 +745,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[three]', + 'Dog.name defer{[label=three;types=[Dog]]}', ] } @@ -513,7 +774,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[another-name-defer]', + 'Dog.name defer{[label=another-name-defer;types=[Dog]]}', ] } @@ -542,7 +803,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[another-name-defer]', + 'Dog.name defer{[label=another-name-defer;types=[Dog]]}', ] } @@ -571,10 +832,24 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[name-defer]', + 'Dog.name defer{[label=name-defer;types=[Dog]]}', ] } + private ExecutableNormalizedOperation createExecutableNormalizedOperations(String query, Map variables) { + assertValidQuery(graphQLSchema, query, variables) + Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.of(variables), + ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true), + ) + } + private List executeQueryAndPrintTree(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) @@ -592,9 +867,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private List printTreeWithIncrementalExecutionDetails(ExecutableNormalizedOperation queryExecutionTree) { def result = [] - Traverser traverser = Traverser.depthFirst({ it.getChildren() }) - - def normalizedFieldToDeferExecution = queryExecutionTree.normalizedFieldToDeferExecution + Traverser traverser = Traverser.depthFirst({ it.getChildren() }) traverser.traverse(queryExecutionTree.getTopLevelFields(), new TraverserVisitorStub() { @Override @@ -605,25 +878,33 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } String printDeferExecutionDetails(ExecutableNormalizedField field) { - def deferExecutions = normalizedFieldToDeferExecution.get(field) + def deferExecutions = field.deferExecutions if (deferExecutions == null || deferExecutions.isEmpty()) { return "" } - def deferLabels = deferExecutions - .collect { it.label } + def deferLabels = new ArrayList<>(deferExecutions) + .sort { it.deferBlock.label } + .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } .join(",") - return " defer[" + deferLabels + "]" + return " defer{${deferLabels}}" } }) result } - private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { + private static void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() def ei = ExecutionInput.newExecutionInput(query).variables(variables).build() assert graphQL.execute(ei).errors.size() == 0 } + + private static ExecutableNormalizedField findField(ExecutableNormalizedOperation operation, String objectTypeNames, String fieldName) { + return operation.normalizedFieldToMergedField + .collect { it.key } + .find { it.fieldName == fieldName + && it.objectTypeNamesToString() == objectTypeNames} + } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 090fc26413..8d7f9e91a3 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -75,7 +75,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -89,33 +89,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } - def "@defer directives are not generated when map is null"() { - String query = """ - query q { - dog { - name - ... @defer(label: "breed-defer") { - breed - } - } - } - """ - GraphQLSchema schema = mkSchema(sdl) - def tree = createNormalizedTree(schema, query) - def normalizedFieldToDeferExecution = null - when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, normalizedFieldToDeferExecution) - def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) - then: - printed == '''{ - dog { - breed - name - } -} -''' - } - def "simple defer with named spread"() { String query = """ query q { @@ -130,7 +103,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -161,7 +134,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -195,7 +168,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -225,7 +198,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -259,7 +232,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -296,7 +269,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -325,7 +298,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -357,7 +330,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -392,7 +365,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -443,7 +416,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -493,7 +466,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 99079c8705..8b35c67b09 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -1,6 +1,5 @@ package graphql.normalized -import com.google.common.collect.ImmutableListMultimap import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables @@ -12,7 +11,6 @@ import graphql.language.Field import graphql.language.IntValue import graphql.language.OperationDefinition import graphql.language.StringValue -import graphql.normalized.incremental.DeferExecution import graphql.parser.Parser import graphql.schema.GraphQLSchema import graphql.schema.idl.RuntimeWiring @@ -205,7 +203,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat def tree = createNormalizedTree(schema, query) when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -256,7 +254,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -337,7 +335,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -429,7 +427,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -524,7 +522,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -591,7 +589,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -648,7 +646,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -706,7 +704,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -773,7 +771,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -871,7 +869,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -969,7 +967,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1285,7 +1283,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat when: - def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables, null) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) OperationDefinition operationDefinition = result.document.getDefinitionsOfType(OperationDefinition.class)[0] def fooField = (Field) operationDefinition.selectionSet.children[0] def nameField = (Field) fooField.selectionSet.children[0] @@ -2175,18 +2173,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat List topLevelFields, VariablePredicate variablePredicate ) { - return localCompileToDocument(schema, operationKind, operationName, topLevelFields, variablePredicate, null) - } - - private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( - GraphQLSchema schema, - OperationDefinition.Operation operationKind, - String operationName, - List topLevelFields, - VariablePredicate variablePredicate, - Map> normalizedFieldToDeferExecution - ) { - return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution) + return localCompileToDocument(schema, operationKind, operationName, topLevelFields,Map.of(), variablePredicate); } private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( @@ -2195,11 +2182,10 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat String operationName, List topLevelFields, Map normalizedFieldToQueryDirectives, - VariablePredicate variablePredicate, - Map> normalizedFieldToDeferExecution + VariablePredicate variablePredicate ) { if (deferSupport) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution) + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } From 0025495b5f0437a5de9b2dc875da826f7ebcfb19 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 8 Jan 2024 18:18:29 +1100 Subject: [PATCH 069/393] WIP --- .../ExecutableNormalizedFieldTest.groovy | 3 +- ...NormalizedOperationFactoryDeferTest.groovy | 35 +++++++- ...tableNormalizedOperationFactoryTest.groovy | 88 ++++++++----------- ...izedOperationToAstCompilerDeferTest.groovy | 3 +- ...ormalizedOperationToAstCompilerTest.groovy | 3 +- 5 files changed, 71 insertions(+), 61 deletions(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy index dc3db5daa4..6debbb2ff2 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy @@ -48,8 +48,7 @@ class ExecutableNormalizedFieldTest extends Specification { """ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory normalizedOperationFactory = new ExecutableNormalizedOperationFactory() - def normalizedOperation = normalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def normalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def pets = normalizedOperation.getTopLevelFields()[0] def allChildren = pets.getChildren() diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index eb2260ee11..85a4fc85f6 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -143,6 +143,35 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "fragments on non-conditional fields - andi"() { + given: + + String query = ''' + query q { + dog { + ... @defer { + name + age + } + ... @defer { + age + } + } + } + ''' +// This should result in age being on its own deferBlock + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + "Dog.name defer{[label=null;types=[Dog]]}", + "Dog.age defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}", + ] + } + def "fragments on subset of non-conditional fields"() { given: @@ -839,9 +868,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private ExecutableNormalizedOperation createExecutableNormalizedOperations(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, null, @@ -853,9 +881,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private List executeQueryAndPrintTree(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, null, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 432075a363..9a45a413db 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -200,7 +200,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -280,7 +279,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -331,7 +329,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -374,7 +371,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -424,7 +420,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -487,7 +482,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -533,7 +527,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -577,7 +571,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -621,7 +615,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -653,7 +647,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -704,7 +698,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -754,7 +748,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -793,7 +787,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -837,7 +831,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -877,7 +870,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -925,7 +917,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1028,7 +1019,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def subFooField = (document.getDefinitions()[1] as FragmentDefinition).getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1071,7 +1062,7 @@ type Dog implements Animal{ def petsField = (document.getDefinitions()[0] as OperationDefinition).getSelectionSet().getSelections()[0] as Field def idField = petsField.getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1120,7 +1111,7 @@ type Dog implements Animal{ def schemaField = selections[2] as Field def typeField = selections[3] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1177,7 +1168,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1220,7 +1211,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -1248,7 +1239,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def normalizedFieldToMergedField = tree.getNormalizedFieldToMergedField() Traverser traverser = Traverser.depthFirst({ it.getChildren() }) @@ -1286,7 +1277,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) @@ -1387,7 +1378,7 @@ schema { Document document = TestUtil.parseQuery(mutation) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1437,7 +1428,7 @@ schema { assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def variables = [ var1: [bar: 123], var2: [foo: "foo", input2: [bar: 123]] @@ -1484,7 +1475,6 @@ schema { assertValidQuery(graphQLSchema, query) def document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) @@ -1519,7 +1509,6 @@ schema { assertValidQuery(graphQLSchema, query) def document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() def variables = [ varIds : null, otherVar: null, @@ -1575,7 +1564,7 @@ schema { ] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) @@ -1628,7 +1617,7 @@ schema { ] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) @@ -1683,7 +1672,7 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) @@ -1741,7 +1730,7 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1789,7 +1778,7 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1865,7 +1854,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -1929,7 +1918,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -1986,7 +1975,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2061,7 +2050,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2123,7 +2112,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2165,7 +2154,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2208,7 +2197,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2251,7 +2240,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2326,7 +2315,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2402,7 +2391,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2464,7 +2453,7 @@ schema { def variables = ["true": Boolean.TRUE, "false": Boolean.FALSE] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) println String.join("\n", printTree(tree)) @@ -2521,7 +2510,7 @@ schema { def variables = [:] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def printedTree = printTreeAndDirectives(tree) @@ -2586,7 +2575,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2639,7 +2628,6 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2686,7 +2674,6 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2884,10 +2871,9 @@ fragment personName on Person { String operationName, CoercedVariables coercedVariableValues ) { - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) } private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( @@ -2896,10 +2882,10 @@ fragment personName on Person { String operationName, RawVariables rawVariables ) { - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, operationName, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 8d7f9e91a3..dab369b974 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -491,9 +491,8 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true) - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( schema, originalDocument, null, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 8b35c67b09..27c4c89a6d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2145,9 +2145,8 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables), options) + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables), options) } private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { From ea786d6d95aa435c52c9368deb4673f17649566b Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 8 Jan 2024 20:11:58 +1100 Subject: [PATCH 070/393] Refactor ENF Factory - Create an inner class to maintain state, so we don't need to pass variables around that much --- .../ExecutableNormalizedOperationFactory.java | 761 +++++++++--------- .../FieldCollectorNormalizedQueryParams.java | 136 ---- .../ExecutableNormalizedFieldTest.groovy | 3 +- ...tableNormalizedOperationFactoryTest.groovy | 192 ++--- ...ormalizedOperationToAstCompilerTest.groovy | 3 +- 5 files changed, 476 insertions(+), 619 deletions(-) delete mode 100644 src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 1124c9b7ce..a635d58571 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -34,7 +34,6 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; @@ -65,6 +64,7 @@ */ @PublicApi public class ExecutableNormalizedOperationFactory { + public static class Options { private final GraphQLContext graphQLContext; private final Locale locale; @@ -91,6 +91,7 @@ public static Options defaultOptions() { * e.g. can be passed to {@link graphql.schema.Coercing} for parsing. * * @param locale the locale to use + * * @return new options object to use */ public Options locale(Locale locale) { @@ -103,6 +104,7 @@ public Options locale(Locale locale) { * Can be used to intercept input values e.g. using {@link graphql.execution.values.InputInterceptor}. * * @param graphQLContext the context to use + * * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { @@ -114,6 +116,7 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * against malicious operations. * * @param maxChildrenDepth the max depth + * * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { @@ -122,6 +125,7 @@ public Options maxChildrenDepth(int maxChildrenDepth) { /** * @return context to use during operation parsing + * * @see #graphQLContext(GraphQLContext) */ public GraphQLContext getGraphQLContext() { @@ -130,6 +134,7 @@ public GraphQLContext getGraphQLContext() { /** * @return locale to use during operation parsing + * * @see #locale(Locale) */ public Locale getLocale() { @@ -138,6 +143,7 @@ public Locale getLocale() { /** * @return maximum children depth before aborting parsing + * * @see #maxChildrenDepth(int) */ public int getMaxChildrenDepth() { @@ -145,7 +151,8 @@ public int getMaxChildrenDepth() { } } - private final ConditionalNodes conditionalNodes = new ConditionalNodes(); + + private static final ConditionalNodes conditionalNodes = new ConditionalNodes(); /** * This will create a runtime representation of the graphql operation that would be executed @@ -165,12 +172,13 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( CoercedVariables coercedVariableValues ) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, + + return createExecutableNormalizedOperation( + graphQLSchema, getOperationResult.operationDefinition, getOperationResult.fragmentsByName, - coercedVariableValues, - null, - Options.defaultOptions()); + coercedVariableValues + ); } /** @@ -188,12 +196,14 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( OperationDefinition operationDefinition, Map fragments, CoercedVariables coercedVariableValues) { - return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, + return new ExecutableNormalizedOperationFactoryInner( + graphQLSchema, operationDefinition, fragments, coercedVariableValues, null, - Options.defaultOptions()); + Options.defaultOptions() + ).createNormalizedQueryImpl(); } /** @@ -267,20 +277,8 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW RawVariables rawVariables, Options options) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); + OperationDefinition operationDefinition = getOperationResult.operationDefinition; - return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, - getOperationResult.operationDefinition, - getOperationResult.fragmentsByName, - rawVariables, - options - ); - } - - private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWithRawVariables(GraphQLSchema graphQLSchema, - OperationDefinition operationDefinition, - Map fragments, - RawVariables rawVariables, - Options options) { List variableDefinitions = operationDefinition.getVariableDefinitions(); CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, @@ -292,437 +290,434 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit rawVariables, options.getGraphQLContext(), options.getLocale()); - return createNormalizedQueryImpl(graphQLSchema, + + return new ExecutableNormalizedOperationFactoryInner( + graphQLSchema, operationDefinition, - fragments, + getOperationResult.fragmentsByName, coercedVariableValues, normalizedVariableValues, - options); - } - - /** - * Creates a new ExecutableNormalizedOperation for the provided query - */ - private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema graphQLSchema, - OperationDefinition operationDefinition, - Map fragments, - CoercedVariables coercedVariableValues, - @Nullable Map normalizedVariableValues, - Options options) { - FieldCollectorNormalizedQueryParams parameters = FieldCollectorNormalizedQueryParams - .newParameters() - .fragments(fragments) - .schema(graphQLSchema) - .coercedVariables(coercedVariableValues.toMap()) - .normalizedVariables(normalizedVariableValues) - .build(); - - GraphQLObjectType rootType = SchemaUtil.getOperationRootType(graphQLSchema, operationDefinition); - - CollectNFResult collectFromOperationResult = collectFromOperation(parameters, operationDefinition, rootType); - - ImmutableListMultimap.Builder fieldToNormalizedField = ImmutableListMultimap.builder(); - ImmutableMap.Builder normalizedFieldToMergedField = ImmutableMap.builder(); - ImmutableMap.Builder normalizedFieldToQueryDirectives = ImmutableMap.builder(); - ImmutableListMultimap.Builder coordinatesToNormalizedFields = ImmutableListMultimap.builder(); - - BiConsumer captureMergedField = (enf, mergedFld) -> { - // QueryDirectivesImpl is a lazy object and only computes itself when asked for - QueryDirectives queryDirectives = new QueryDirectivesImpl(mergedFld, graphQLSchema, coercedVariableValues.toMap(), options.getGraphQLContext(), options.getLocale()); - normalizedFieldToQueryDirectives.put(enf, queryDirectives); - normalizedFieldToMergedField.put(enf, mergedFld); - }; - - for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { - ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); - MergedField mergedField = newMergedField(fieldAndAstParents); - - captureMergedField.accept(topLevel, mergedField); - - updateFieldToNFMap(topLevel, fieldAndAstParents, fieldToNormalizedField); - updateCoordinatedToNFMap(coordinatesToNormalizedFields, topLevel); - - buildFieldWithChildren( - topLevel, - fieldAndAstParents, - parameters, - fieldToNormalizedField, - captureMergedField, - coordinatesToNormalizedFields, - 1, - options.getMaxChildrenDepth()); - } - for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { - List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema); - } - return new ExecutableNormalizedOperation( - operationDefinition.getOperation(), - operationDefinition.getName(), - new ArrayList<>(collectFromOperationResult.children), - fieldToNormalizedField.build(), - normalizedFieldToMergedField.build(), - normalizedFieldToQueryDirectives.build(), - coordinatesToNormalizedFields.build() - ); + options + ).createNormalizedQueryImpl(); } - private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, - ImmutableList fieldAndAstParents, - FieldCollectorNormalizedQueryParams fieldCollectorNormalizedQueryParams, - ImmutableListMultimap.Builder fieldNormalizedField, - BiConsumer captureMergedField, - ImmutableListMultimap.Builder coordinatesToNormalizedFields, - int curLevel, - int maxLevel) { - if (curLevel > maxLevel) { - throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); + private static class ExecutableNormalizedOperationFactoryInner { + private final GraphQLSchema graphQLSchema; + private final OperationDefinition operationDefinition; + private final Map fragments; + private final CoercedVariables coercedVariableValues; + private final @Nullable Map normalizedVariableValues; + private final Options options; + + private final List possibleMergerList = new ArrayList<>(); + + private final ImmutableListMultimap.Builder fieldToNormalizedField = ImmutableListMultimap.builder(); + private final ImmutableMap.Builder normalizedFieldToMergedField = ImmutableMap.builder(); + private final ImmutableMap.Builder normalizedFieldToQueryDirectives = ImmutableMap.builder(); + private final ImmutableListMultimap.Builder coordinatesToNormalizedFields = ImmutableListMultimap.builder(); + + private ExecutableNormalizedOperationFactoryInner( + GraphQLSchema graphQLSchema, + OperationDefinition operationDefinition, + Map fragments, + CoercedVariables coercedVariableValues, + @Nullable Map normalizedVariableValues, + Options options + ) { + this.graphQLSchema = graphQLSchema; + this.operationDefinition = operationDefinition; + this.fragments = fragments; + this.coercedVariableValues = coercedVariableValues; + this.normalizedVariableValues = normalizedVariableValues; + this.options = options; } - CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1); + /** + * Creates a new ExecutableNormalizedOperation for the provided query + */ + private ExecutableNormalizedOperation createNormalizedQueryImpl() { + GraphQLObjectType rootType = SchemaUtil.getOperationRootType(graphQLSchema, operationDefinition); - for (ExecutableNormalizedField childENF : nextLevel.children) { - executableNormalizedField.addChild(childENF); - ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); + CollectNFResult collectFromOperationResult = collectFromOperation(rootType); - MergedField mergedField = newMergedField(childFieldAndAstParents); - captureMergedField.accept(childENF, mergedField); + BiConsumer captureMergedField = (enf, mergedFld) -> { + // QueryDirectivesImpl is a lazy object and only computes itself when asked for + QueryDirectives queryDirectives = new QueryDirectivesImpl(mergedFld, graphQLSchema, coercedVariableValues.toMap(), options.getGraphQLContext(), options.getLocale()); + normalizedFieldToQueryDirectives.put(enf, queryDirectives); + normalizedFieldToMergedField.put(enf, mergedFld); + }; - updateFieldToNFMap(childENF, childFieldAndAstParents, fieldNormalizedField); - updateCoordinatedToNFMap(coordinatesToNormalizedFields, childENF); + for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { + ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); + MergedField mergedField = newMergedField(fieldAndAstParents); - buildFieldWithChildren(childENF, - childFieldAndAstParents, - fieldCollectorNormalizedQueryParams, - fieldNormalizedField, - captureMergedField, - coordinatesToNormalizedFields, - curLevel + 1, - maxLevel); - } - } + captureMergedField.accept(topLevel, mergedField); - private static MergedField newMergedField(ImmutableList fieldAndAstParents) { - return MergedField.newMergedField(map(fieldAndAstParents, fieldAndAstParent -> fieldAndAstParent.field)).build(); - } + updateFieldToNFMap(topLevel, fieldAndAstParents); + updateCoordinatedToNFMap(topLevel); - private void updateFieldToNFMap(ExecutableNormalizedField executableNormalizedField, - ImmutableList mergedField, - ImmutableListMultimap.Builder fieldToNormalizedField) { - for (FieldAndAstParent astField : mergedField) { - fieldToNormalizedField.put(astField.field, executableNormalizedField); + buildFieldWithChildren( + topLevel, + fieldAndAstParents, + captureMergedField, + 1, + options.getMaxChildrenDepth()); + } + // getPossibleMergerList + for (PossibleMerger possibleMerger : possibleMergerList) { + List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema); + } + return new ExecutableNormalizedOperation( + operationDefinition.getOperation(), + operationDefinition.getName(), + new ArrayList<>(collectFromOperationResult.children), + fieldToNormalizedField.build(), + normalizedFieldToMergedField.build(), + normalizedFieldToQueryDirectives.build(), + coordinatesToNormalizedFields.build() + ); } - } - private void updateCoordinatedToNFMap(ImmutableListMultimap.Builder coordinatesToNormalizedFields, ExecutableNormalizedField topLevel) { - for (String objectType : topLevel.getObjectTypeNames()) { - FieldCoordinates coordinates = FieldCoordinates.coordinates(objectType, topLevel.getFieldName()); - coordinatesToNormalizedFields.put(coordinates, topLevel); - } - } - private static class FieldAndAstParent { - final Field field; - final GraphQLCompositeType astParentType; + private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, + ImmutableList fieldAndAstParents, + BiConsumer captureMergedField, + int curLevel, + int maxLevel) { + if (curLevel > maxLevel) { + throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); + } - private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { - this.field = field; - this.astParentType = astParentType; - } - } + CollectNFResult nextLevel = collectFromMergedField(executableNormalizedField, fieldAndAstParents, curLevel + 1); + for (ExecutableNormalizedField childENF : nextLevel.children) { + executableNormalizedField.addChild(childENF); + ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); - public static class CollectNFResult { - private final Collection children; - private final ImmutableListMultimap normalizedFieldToAstFields; + MergedField mergedField = newMergedField(childFieldAndAstParents); + captureMergedField.accept(childENF, mergedField); - public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields) { - this.children = children; - this.normalizedFieldToAstFields = normalizedFieldToAstFields; - } - } + updateFieldToNFMap(childENF, childFieldAndAstParents); + updateCoordinatedToNFMap(childENF); + buildFieldWithChildren(childENF, + childFieldAndAstParents, + captureMergedField, + curLevel + 1, + maxLevel); + } + } - public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParams parameters, - ExecutableNormalizedField executableNormalizedField, - ImmutableList mergedField, - int level) { - List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); - Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); - if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of()); + private static MergedField newMergedField(ImmutableList fieldAndAstParents) { + return MergedField.newMergedField(map(fieldAndAstParents, fieldAndAstParent -> fieldAndAstParent.field)).build(); } - List collectedFields = new ArrayList<>(); - for (FieldAndAstParent fieldAndAstParent : mergedField) { - if (fieldAndAstParent.field.getSelectionSet() == null) { - continue; + private void updateFieldToNFMap(ExecutableNormalizedField executableNormalizedField, + ImmutableList mergedField) { + for (FieldAndAstParent astField : mergedField) { + fieldToNormalizedField.put(astField.field, executableNormalizedField); } - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(parameters.getGraphQLSchema(), fieldAndAstParent.astParentType, fieldAndAstParent.field.getName()); - GraphQLUnmodifiedType astParentType = unwrapAll(fieldDefinition.getType()); - this.collectFromSelectionSet(parameters, - fieldAndAstParent.field.getSelectionSet(), - collectedFields, - (GraphQLCompositeType) astParentType, - possibleObjects - ); } - Map> fieldsByName = fieldsByResultKey(collectedFields); - ImmutableList.Builder resultNFs = ImmutableList.builder(); - ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); - } - - private Map> fieldsByResultKey(List collectedFields) { - Map> fieldsByName = new LinkedHashMap<>(); - for (CollectedField collectedField : collectedFields) { - fieldsByName.computeIfAbsent(collectedField.field.getResultKey(), ignored -> new ArrayList<>()).add(collectedField); + private void updateCoordinatedToNFMap(ExecutableNormalizedField topLevel) { + for (String objectType : topLevel.getObjectTypeNames()) { + FieldCoordinates coordinates = FieldCoordinates.coordinates(objectType, topLevel.getFieldName()); + coordinatesToNormalizedFields.put(coordinates, topLevel); + } } - return fieldsByName; - } - - public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, - OperationDefinition operationDefinition, - GraphQLObjectType rootType) { + public CollectNFResult collectFromMergedField(ExecutableNormalizedField executableNormalizedField, + ImmutableList mergedField, + int level) { + List fieldDefs = executableNormalizedField.getFieldDefinitions(graphQLSchema); + Set possibleObjects = resolvePossibleObjects(fieldDefs); + if (possibleObjects.isEmpty()) { + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of()); + } - Set possibleObjects = ImmutableSet.of(rootType); - List collectedFields = new ArrayList<>(); - collectFromSelectionSet(parameters, operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects); - // group by result key - Map> fieldsByName = fieldsByResultKey(collectedFields); - ImmutableList.Builder resultNFs = ImmutableList.builder(); - ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); + List collectedFields = new ArrayList<>(); + for (FieldAndAstParent fieldAndAstParent : mergedField) { + if (fieldAndAstParent.field.getSelectionSet() == null) { + continue; + } + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(graphQLSchema, fieldAndAstParent.astParentType, fieldAndAstParent.field.getName()); + GraphQLUnmodifiedType astParentType = unwrapAll(fieldDefinition.getType()); + this.collectFromSelectionSet(fieldAndAstParent.field.getSelectionSet(), + collectedFields, + (GraphQLCompositeType) astParentType, + possibleObjects + ); + } + Map> fieldsByName = fieldsByResultKey(collectedFields); + ImmutableList.Builder resultNFs = ImmutableList.builder(); + ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null); + createNFs(resultNFs, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); - } + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + } - private void createNFs(ImmutableList.Builder nfListBuilder, - FieldCollectorNormalizedQueryParams parameters, - Map> fieldsByName, - ImmutableListMultimap.Builder normalizedFieldToAstFields, - int level, - ExecutableNormalizedField parent) { - for (String resultKey : fieldsByName.keySet()) { - List fieldsWithSameResultKey = fieldsByName.get(resultKey); - List commonParentsGroups = groupByCommonParents(fieldsWithSameResultKey); - for (CollectedFieldGroup fieldGroup : commonParentsGroups) { - ExecutableNormalizedField nf = createNF(parameters, fieldGroup, level, parent); - if (nf == null) { - continue; + private Map> fieldsByResultKey(List collectedFields) { + Map> fieldsByName = new LinkedHashMap<>(); + for (CollectedField collectedField : collectedFields) { + fieldsByName.computeIfAbsent(collectedField.field.getResultKey(), ignored -> new ArrayList<>()).add(collectedField); + } + return fieldsByName; + } + + public CollectNFResult collectFromOperation(GraphQLObjectType rootType) { + + + Set possibleObjects = ImmutableSet.of(rootType); + List collectedFields = new ArrayList<>(); + collectFromSelectionSet(operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects); + // group by result key + Map> fieldsByName = fieldsByResultKey(collectedFields); + ImmutableList.Builder resultNFs = ImmutableList.builder(); + ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); + + createNFs(resultNFs, fieldsByName, normalizedFieldToAstFields, 1, null); + + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + } + + private void createNFs(ImmutableList.Builder nfListBuilder, + Map> fieldsByName, + ImmutableListMultimap.Builder normalizedFieldToAstFields, + int level, + ExecutableNormalizedField parent) { + for (String resultKey : fieldsByName.keySet()) { + List fieldsWithSameResultKey = fieldsByName.get(resultKey); + List commonParentsGroups = groupByCommonParents(fieldsWithSameResultKey); + for (CollectedFieldGroup fieldGroup : commonParentsGroups) { + ExecutableNormalizedField nf = createNF(fieldGroup, level, parent); + if (nf == null) { + continue; + } + for (CollectedField collectedField : fieldGroup.fields) { + normalizedFieldToAstFields.put(nf, new FieldAndAstParent(collectedField.field, collectedField.astTypeCondition)); + } + nfListBuilder.add(nf); } - for (CollectedField collectedField : fieldGroup.fields) { - normalizedFieldToAstFields.put(nf, new FieldAndAstParent(collectedField.field, collectedField.astTypeCondition)); + if (commonParentsGroups.size() > 1) { + possibleMergerList.add(new PossibleMerger(parent, resultKey)); } - nfListBuilder.add(nf); - } - if (commonParentsGroups.size() > 1) { - parameters.addPossibleMergers(parent, resultKey); } } - } - - private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams parameters, - CollectedFieldGroup collectedFieldGroup, - int level, - ExecutableNormalizedField parent) { - Field field; - Set objectTypes = collectedFieldGroup.objectTypes; - field = collectedFieldGroup.fields.iterator().next().field; - String fieldName = field.getName(); - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(parameters.getGraphQLSchema(), objectTypes.iterator().next(), fieldName); - - Map argumentValues = ValuesResolver.getArgumentValues(fieldDefinition.getArguments(), field.getArguments(), CoercedVariables.of(parameters.getCoercedVariableValues()), parameters.getGraphQLContext(), parameters.getLocale()); - Map normalizedArgumentValues = null; - if (parameters.getNormalizedVariableValues() != null) { - normalizedArgumentValues = ValuesResolver.getNormalizedArgumentValues(fieldDefinition.getArguments(), field.getArguments(), parameters.getNormalizedVariableValues()); - } - ImmutableList objectTypeNames = map(objectTypes, GraphQLObjectType::getName); - - return ExecutableNormalizedField.newNormalizedField() - .alias(field.getAlias()) - .resolvedArguments(argumentValues) - .normalizedArguments(normalizedArgumentValues) - .astArguments(field.getArguments()) - .objectTypeNames(objectTypeNames) - .fieldName(fieldName) - .level(level) - .parent(parent) - .build(); - } - private static class CollectedFieldGroup { - Set objectTypes; - Set fields; + private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGroup, + int level, + ExecutableNormalizedField parent) { + Field field; + Set objectTypes = collectedFieldGroup.objectTypes; + field = collectedFieldGroup.fields.iterator().next().field; + String fieldName = field.getName(); + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(graphQLSchema, objectTypes.iterator().next(), fieldName); - public CollectedFieldGroup(Set fields, Set objectTypes) { - this.fields = fields; - this.objectTypes = objectTypes; + Map argumentValues = ValuesResolver.getArgumentValues(fieldDefinition.getArguments(), field.getArguments(), CoercedVariables.of(this.coercedVariableValues.toMap()), this.options.graphQLContext, this.options.locale); + Map normalizedArgumentValues = null; + if (this.normalizedVariableValues != null) { + normalizedArgumentValues = ValuesResolver.getNormalizedArgumentValues(fieldDefinition.getArguments(), field.getArguments(), this.normalizedVariableValues); + } + ImmutableList objectTypeNames = map(objectTypes, GraphQLObjectType::getName); + + return ExecutableNormalizedField.newNormalizedField() + .alias(field.getAlias()) + .resolvedArguments(argumentValues) + .normalizedArguments(normalizedArgumentValues) + .astArguments(field.getArguments()) + .objectTypeNames(objectTypeNames) + .fieldName(fieldName) + .level(level) + .parent(parent) + .build(); + } + + private static class CollectedFieldGroup { + Set objectTypes; + Set fields; + + public CollectedFieldGroup(Set fields, Set objectTypes) { + this.fields = fields; + this.objectTypes = objectTypes; + } } - } - private List groupByCommonParents(Collection fields) { - ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - for (CollectedField collectedField : fields) { - objectTypes.addAll(collectedField.objectTypes); - } - Set allRelevantObjects = objectTypes.build(); - Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); - if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects)); - } - ImmutableList.Builder result = ImmutableList.builder(); - for (GraphQLObjectType objectType : allRelevantObjects) { - Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType))); + private List groupByCommonParents(Collection fields) { + ImmutableSet.Builder objectTypes = ImmutableSet.builder(); + for (CollectedField collectedField : fields) { + objectTypes.addAll(collectedField.objectTypes); + } + Set allRelevantObjects = objectTypes.build(); + Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); + if (groupByAstParent.size() == 1) { + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects)); + } + ImmutableList.Builder result = ImmutableList.builder(); + for (GraphQLObjectType objectType : allRelevantObjects) { + Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType))); + } + return result.build(); + } + + private void collectFromSelectionSet(SelectionSet selectionSet, + List result, + GraphQLCompositeType astTypeCondition, + Set possibleObjects + ) { + for (Selection selection : selectionSet.getSelections()) { + if (selection instanceof Field) { + collectField(result, (Field) selection, possibleObjects, astTypeCondition); + } else if (selection instanceof InlineFragment) { + collectInlineFragment(result, (InlineFragment) selection, possibleObjects, astTypeCondition); + } else if (selection instanceof FragmentSpread) { + collectFragmentSpread(result, (FragmentSpread) selection, possibleObjects); + } + } } - return result.build(); - } + private void collectFragmentSpread(List result, + FragmentSpread fragmentSpread, + Set possibleObjects + ) { + if (!conditionalNodes.shouldInclude(fragmentSpread, + this.coercedVariableValues.toMap(), + this.graphQLSchema, + this.options.graphQLContext)) { + return; + } + FragmentDefinition fragmentDefinition = assertNotNull(this.fragments.get(fragmentSpread.getName())); - private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams parameters, - SelectionSet selectionSet, - List result, - GraphQLCompositeType astTypeCondition, - Set possibleObjects - ) { - for (Selection selection : selectionSet.getSelections()) { - if (selection instanceof Field) { - collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition); - } else if (selection instanceof InlineFragment) { - collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition); - } else if (selection instanceof FragmentSpread) { - collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects); + if (!conditionalNodes.shouldInclude(fragmentDefinition, + this.coercedVariableValues.toMap(), + this.graphQLSchema, + this.options.graphQLContext)) { + return; } - } - } + GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(this.graphQLSchema.getType(fragmentDefinition.getTypeCondition().getName())); + Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition); + collectFromSelectionSet(fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); + } + + private void collectInlineFragment(List result, + InlineFragment inlineFragment, + Set possibleObjects, + GraphQLCompositeType astTypeCondition + ) { + if (!conditionalNodes.shouldInclude(inlineFragment, this.coercedVariableValues.toMap(), this.graphQLSchema, this.options.graphQLContext)) { + return; + } + Set newPossibleObjects = possibleObjects; + GraphQLCompositeType newAstTypeCondition = astTypeCondition; - private static class CollectedField { - Field field; - Set objectTypes; - GraphQLCompositeType astTypeCondition; + if (inlineFragment.getTypeCondition() != null) { + newAstTypeCondition = (GraphQLCompositeType) this.graphQLSchema.getType(inlineFragment.getTypeCondition().getName()); + newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition); - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition) { - this.field = field; - this.objectTypes = objectTypes; - this.astTypeCondition = astTypeCondition; + } + collectFromSelectionSet(inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); + } + + private void collectField(List result, + Field field, + Set possibleObjectTypes, + GraphQLCompositeType astTypeCondition + ) { + if (!conditionalNodes.shouldInclude(field, + this.coercedVariableValues.toMap(), + this.graphQLSchema, + this.options.graphQLContext)) { + return; + } + // this means there is actually no possible type for this field, and we are done + if (possibleObjectTypes.isEmpty()) { + return; + } + result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition)); } - public boolean isAbstract() { - return GraphQLTypeUtil.isInterfaceOrUnion(astTypeCondition); - } + private Set narrowDownPossibleObjects(Set currentOnes, + GraphQLCompositeType typeCondition) { + + ImmutableSet resolvedTypeCondition = resolvePossibleObjects(typeCondition); + if (currentOnes.isEmpty()) { + return resolvedTypeCondition; + } - public boolean isConcrete() { - return GraphQLTypeUtil.isObjectType(astTypeCondition); + // Faster intersection, as either set often has a size of 1. + return intersection(currentOnes, resolvedTypeCondition); } - } - private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameters, - List result, - FragmentSpread fragmentSpread, - Set possibleObjects - ) { - if (!conditionalNodes.shouldInclude(fragmentSpread, - parameters.getCoercedVariableValues(), - parameters.getGraphQLSchema(), - parameters.getGraphQLContext())) { - return; - } - FragmentDefinition fragmentDefinition = assertNotNull(parameters.getFragmentsByName().get(fragmentSpread.getName())); - - if (!conditionalNodes.shouldInclude(fragmentDefinition, - parameters.getCoercedVariableValues(), - parameters.getGraphQLSchema(), - parameters.getGraphQLContext())) { - return; - } - GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); - Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); - } + private ImmutableSet resolvePossibleObjects(List defs) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (GraphQLFieldDefinition def : defs) { + GraphQLUnmodifiedType outputType = unwrapAll(def.getType()); + if (outputType instanceof GraphQLCompositeType) { + builder.addAll(resolvePossibleObjects((GraphQLCompositeType) outputType)); + } + } - private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameters, - List result, - InlineFragment inlineFragment, - Set possibleObjects, - GraphQLCompositeType astTypeCondition - ) { - if (!conditionalNodes.shouldInclude(inlineFragment, parameters.getCoercedVariableValues(), parameters.getGraphQLSchema(), parameters.getGraphQLContext())) { - return; + return builder.build(); } - Set newPossibleObjects = possibleObjects; - GraphQLCompositeType newAstTypeCondition = astTypeCondition; - - if (inlineFragment.getTypeCondition() != null) { - newAstTypeCondition = (GraphQLCompositeType) parameters.getGraphQLSchema().getType(inlineFragment.getTypeCondition().getName()); - newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); + private ImmutableSet resolvePossibleObjects(GraphQLCompositeType type) { + if (type instanceof GraphQLObjectType) { + return ImmutableSet.of((GraphQLObjectType) type); + } else if (type instanceof GraphQLInterfaceType) { + return ImmutableSet.copyOf(graphQLSchema.getImplementations((GraphQLInterfaceType) type)); + } else if (type instanceof GraphQLUnionType) { + List unionTypes = ((GraphQLUnionType) type).getTypes(); + return ImmutableSet.copyOf(ImmutableKit.map(unionTypes, GraphQLObjectType.class::cast)); + } else { + return assertShouldNeverHappen(); + } } - collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); - } - private void collectField(FieldCollectorNormalizedQueryParams parameters, - List result, - Field field, - Set possibleObjectTypes, - GraphQLCompositeType astTypeCondition - ) { - if (!conditionalNodes.shouldInclude(field, - parameters.getCoercedVariableValues(), - parameters.getGraphQLSchema(), - parameters.getGraphQLContext())) { - return; - } - // this means there is actually no possible type for this field, and we are done - if (possibleObjectTypes.isEmpty()) { - return; + private static class PossibleMerger { + ExecutableNormalizedField parent; + String resultKey; + + public PossibleMerger(ExecutableNormalizedField parent, String resultKey) { + this.parent = parent; + this.resultKey = resultKey; + } } - result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition)); - } - private Set narrowDownPossibleObjects(Set currentOnes, - GraphQLCompositeType typeCondition, - GraphQLSchema graphQLSchema) { + private static class CollectedField { + Field field; + Set objectTypes; + GraphQLCompositeType astTypeCondition; - ImmutableSet resolvedTypeCondition = resolvePossibleObjects(typeCondition, graphQLSchema); - if (currentOnes.isEmpty()) { - return resolvedTypeCondition; + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition) { + this.field = field; + this.objectTypes = objectTypes; + this.astTypeCondition = astTypeCondition; + } } - // Faster intersection, as either set often has a size of 1. - return intersection(currentOnes, resolvedTypeCondition); - } - - private ImmutableSet resolvePossibleObjects(List defs, GraphQLSchema graphQLSchema) { - ImmutableSet.Builder builder = ImmutableSet.builder(); + public static class CollectNFResult { + private final Collection children; + private final ImmutableListMultimap normalizedFieldToAstFields; - for (GraphQLFieldDefinition def : defs) { - GraphQLUnmodifiedType outputType = unwrapAll(def.getType()); - if (outputType instanceof GraphQLCompositeType) { - builder.addAll(resolvePossibleObjects((GraphQLCompositeType) outputType, graphQLSchema)); + public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields) { + this.children = children; + this.normalizedFieldToAstFields = normalizedFieldToAstFields; } } - return builder.build(); - } + private static class FieldAndAstParent { + final Field field; + final GraphQLCompositeType astParentType; - private ImmutableSet resolvePossibleObjects(GraphQLCompositeType type, GraphQLSchema graphQLSchema) { - if (type instanceof GraphQLObjectType) { - return ImmutableSet.of((GraphQLObjectType) type); - } else if (type instanceof GraphQLInterfaceType) { - return ImmutableSet.copyOf(graphQLSchema.getImplementations((GraphQLInterfaceType) type)); - } else if (type instanceof GraphQLUnionType) { - List unionTypes = ((GraphQLUnionType) type).getTypes(); - return ImmutableSet.copyOf(ImmutableKit.map(unionTypes, GraphQLObjectType.class::cast)); - } else { - return assertShouldNeverHappen(); + private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { + this.field = field; + this.astParentType = astParentType; + } } } + } diff --git a/src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java b/src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java deleted file mode 100644 index 8b4d03bf3e..0000000000 --- a/src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java +++ /dev/null @@ -1,136 +0,0 @@ -package graphql.normalized; - -import graphql.Assert; -import graphql.GraphQLContext; -import graphql.Internal; -import graphql.language.FragmentDefinition; -import graphql.schema.GraphQLSchema; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -@Internal -public class FieldCollectorNormalizedQueryParams { - private final GraphQLSchema graphQLSchema; - private final Map fragmentsByName; - private final Map coercedVariableValues; - private final Map normalizedVariableValues; - private final GraphQLContext graphQLContext; - private final Locale locale; - - private final List possibleMergerList = new ArrayList<>(); - - public static class PossibleMerger { - ExecutableNormalizedField parent; - String resultKey; - - public PossibleMerger(ExecutableNormalizedField parent, String resultKey) { - this.parent = parent; - this.resultKey = resultKey; - } - } - - public void addPossibleMergers(ExecutableNormalizedField parent, String resultKey) { - possibleMergerList.add(new PossibleMerger(parent, resultKey)); - } - - public List getPossibleMergerList() { - return possibleMergerList; - } - - public GraphQLSchema getGraphQLSchema() { - return graphQLSchema; - } - - public Map getFragmentsByName() { - return fragmentsByName; - } - - @NotNull - public Map getCoercedVariableValues() { - return coercedVariableValues; - } - - @Nullable - public Map getNormalizedVariableValues() { - return normalizedVariableValues; - } - - public GraphQLContext getGraphQLContext() { - return graphQLContext; - } - - public Locale getLocale() { - return locale; - } - - private FieldCollectorNormalizedQueryParams(Builder builder) { - this.fragmentsByName = builder.fragmentsByName; - this.graphQLSchema = builder.graphQLSchema; - this.coercedVariableValues = builder.coercedVariableValues; - this.normalizedVariableValues = builder.normalizedVariableValues; - this.graphQLContext = builder.graphQLContext; - this.locale = builder.locale; - } - - public static Builder newParameters() { - return new Builder(); - } - - public static class Builder { - private GraphQLSchema graphQLSchema; - private final Map fragmentsByName = new LinkedHashMap<>(); - private final Map coercedVariableValues = new LinkedHashMap<>(); - private Map normalizedVariableValues; - private GraphQLContext graphQLContext = GraphQLContext.getDefault(); - private Locale locale = Locale.getDefault(); - - /** - * @see FieldCollectorNormalizedQueryParams#newParameters() - */ - private Builder() { - - } - - public Builder schema(GraphQLSchema graphQLSchema) { - this.graphQLSchema = graphQLSchema; - return this; - } - - public Builder fragments(Map fragmentsByName) { - this.fragmentsByName.putAll(fragmentsByName); - return this; - } - - public Builder coercedVariables(Map coercedVariableValues) { - this.coercedVariableValues.putAll(coercedVariableValues); - return this; - } - - public Builder normalizedVariables(Map normalizedVariableValues) { - this.normalizedVariableValues = normalizedVariableValues; - return this; - } - - public Builder graphQLContext(GraphQLContext graphQLContext) { - this.graphQLContext = graphQLContext; - return this; - } - - public Builder locale(Locale locale) { - this.locale = locale; - return this; - } - - public FieldCollectorNormalizedQueryParams build() { - Assert.assertNotNull(graphQLSchema, () -> "You must provide a schema"); - return new FieldCollectorNormalizedQueryParams(this); - } - - } -} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy index dc3db5daa4..6debbb2ff2 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy @@ -48,8 +48,7 @@ class ExecutableNormalizedFieldTest extends Specification { """ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory normalizedOperationFactory = new ExecutableNormalizedOperationFactory() - def normalizedOperation = normalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def normalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def pets = normalizedOperation.getTopLevelFields()[0] def allChildren = pets.getChildren() diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 89ea656b81..7a67201bd9 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -112,8 +112,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -198,8 +198,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -278,8 +278,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -329,8 +329,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -372,8 +372,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -422,8 +422,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -485,8 +485,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -531,8 +531,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -575,8 +575,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -619,8 +619,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -651,8 +651,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -702,8 +702,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -752,8 +752,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -791,8 +791,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -835,8 +835,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -875,8 +875,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -923,8 +923,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1026,8 +1026,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def subFooField = (document.getDefinitions()[1] as FragmentDefinition).getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() expect: @@ -1069,8 +1069,8 @@ type Dog implements Animal{ def petsField = (document.getDefinitions()[0] as OperationDefinition).getSelectionSet().getSelections()[0] as Field def idField = petsField.getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1118,8 +1118,8 @@ type Dog implements Animal{ def schemaField = selections[2] as Field def typeField = selections[3] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() expect: @@ -1175,8 +1175,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1218,8 +1218,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -1246,8 +1246,8 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def normalizedFieldToMergedField = tree.getNormalizedFieldToMergedField() Traverser traverser = Traverser.depthFirst({ it.getChildren() }) List result = new ArrayList<>() @@ -1284,10 +1284,10 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def coordinatesToNormalizedFields = tree.coordinatesToNormalizedFields then: @@ -1385,8 +1385,8 @@ schema { Document document = TestUtil.parseQuery(mutation) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1435,7 +1435,7 @@ schema { assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def variables = [ var1: [bar: 123], var2: [foo: "foo", input2: [bar: 123]] @@ -1443,7 +1443,7 @@ schema { // the normalized arg value should be the same regardless of how the value was provided def expectedNormalizedArgValue = [foo: new NormalizedInputValue("String", parseValue('"foo"')), input2: new NormalizedInputValue("Input2", [bar: new NormalizedInputValue("Int", parseValue("123"))])] when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def secondField = topLevelField.getChildren().get(0) def arg1 = secondField.getNormalizedArgument("arg1") @@ -1482,9 +1482,9 @@ schema { assertValidQuery(graphQLSchema, query) def document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) then: def topLevelField = tree.getTopLevelFields().get(0) @@ -1517,13 +1517,13 @@ schema { assertValidQuery(graphQLSchema, query) def document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def variables = [ varIds : null, otherVar: null, ] when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) then: def topLevelField = tree.getTopLevelFields().get(0) @@ -1573,9 +1573,9 @@ schema { ] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def arg1 = topLevelField.getNormalizedArgument("arg1") def arg2 = topLevelField.getNormalizedArgument("arg2") @@ -1626,9 +1626,9 @@ schema { ] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def arg1 = topLevelField.getNormalizedArgument("arg1") def arg2 = topLevelField.getNormalizedArgument("arg2") @@ -1681,9 +1681,9 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) then: tree.normalizedFieldToMergedField.size() == 3 @@ -1739,9 +1739,9 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) then: @@ -1787,9 +1787,9 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) then: @@ -1863,9 +1863,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -1927,9 +1927,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -1984,9 +1984,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2059,9 +2059,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2121,9 +2121,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2163,9 +2163,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2206,9 +2206,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2249,9 +2249,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2324,9 +2324,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2400,9 +2400,9 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2462,9 +2462,9 @@ schema { def variables = ["true": Boolean.TRUE, "false": Boolean.FALSE] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) println String.join("\n", printTree(tree)) def printedTree = printTree(tree) @@ -2519,9 +2519,9 @@ schema { def variables = [:] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def printedTree = printTreeAndDirectives(tree) then: @@ -2584,8 +2584,8 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2637,8 +2637,8 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2684,8 +2684,8 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index dd074ce7a8..e990d981a5 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2140,8 +2140,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) } private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { From 1c96e10b9ca478b1ec336dd052a485536a9533f7 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 8 Jan 2024 20:27:22 +1100 Subject: [PATCH 071/393] Convert captureMergedField into a class method --- .../ExecutableNormalizedOperationFactory.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index a635d58571..3fc18873e9 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -341,18 +341,11 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { CollectNFResult collectFromOperationResult = collectFromOperation(rootType); - BiConsumer captureMergedField = (enf, mergedFld) -> { - // QueryDirectivesImpl is a lazy object and only computes itself when asked for - QueryDirectives queryDirectives = new QueryDirectivesImpl(mergedFld, graphQLSchema, coercedVariableValues.toMap(), options.getGraphQLContext(), options.getLocale()); - normalizedFieldToQueryDirectives.put(enf, queryDirectives); - normalizedFieldToMergedField.put(enf, mergedFld); - }; - for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); MergedField mergedField = newMergedField(fieldAndAstParents); - captureMergedField.accept(topLevel, mergedField); + captureMergedField(topLevel, mergedField); updateFieldToNFMap(topLevel, fieldAndAstParents); updateCoordinatedToNFMap(topLevel); @@ -360,7 +353,6 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { buildFieldWithChildren( topLevel, fieldAndAstParents, - captureMergedField, 1, options.getMaxChildrenDepth()); } @@ -380,10 +372,15 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { ); } + private void captureMergedField(ExecutableNormalizedField enf, MergedField mergedFld) { + // QueryDirectivesImpl is a lazy object and only computes itself when asked for + QueryDirectives queryDirectives = new QueryDirectivesImpl(mergedFld, graphQLSchema, coercedVariableValues.toMap(), options.getGraphQLContext(), options.getLocale()); + normalizedFieldToQueryDirectives.put(enf, queryDirectives); + normalizedFieldToMergedField.put(enf, mergedFld); + } private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, ImmutableList fieldAndAstParents, - BiConsumer captureMergedField, int curLevel, int maxLevel) { if (curLevel > maxLevel) { @@ -397,14 +394,13 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); MergedField mergedField = newMergedField(childFieldAndAstParents); - captureMergedField.accept(childENF, mergedField); + captureMergedField(childENF, mergedField); updateFieldToNFMap(childENF, childFieldAndAstParents); updateCoordinatedToNFMap(childENF); buildFieldWithChildren(childENF, childFieldAndAstParents, - captureMergedField, curLevel + 1, maxLevel); } From f1ff2044236e407faac9b2a464dd9041ce0866d8 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 8 Jan 2024 20:32:01 +1100 Subject: [PATCH 072/393] Marked the constructor of ENFFactory as deprecated --- .../normalized/ExecutableNormalizedOperationFactory.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 3fc18873e9..2f878fd3f9 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.collect.ImmutableKit; @@ -151,6 +152,14 @@ public int getMaxChildrenDepth() { } } + /** + * @deprecated the API surface of this class is all static methods, eventually this constructor will be made `private`. + */ + @Deprecated + @DeprecatedAt("2024-01-08") + public ExecutableNormalizedOperationFactory() { + + } private static final ConditionalNodes conditionalNodes = new ConditionalNodes(); From 02374cfd72c87ef2e8e204ac467cd2a03afb1bcf Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:10:17 +0900 Subject: [PATCH 073/393] Reproduce problem with test --- .../execution/ConditionalNodesTest.groovy | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy index 629f1fde98..5b33a316b2 100644 --- a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy +++ b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy @@ -111,10 +111,18 @@ class ConditionalNodesTest extends Specification { type Query { in : String out : String + pet : Pet + } + + type Pet { + name: String + favouriteSnack: String } """ DataFetcher df = { DataFetchingEnvironment env -> env.getFieldDefinition().name } - def graphQL = TestUtil.graphQL(sdl, [Query: ["in": df, "out": df]]).build() + def graphQL = TestUtil.graphQL(sdl, [ + Query: ["in": df, "out": df, "pet": (DataFetcher) { [ : ] } ], + Pet: ["name": df, "favouriteSnack": df]]).build() ConditionalNodeDecision customDecision = new ConditionalNodeDecision() { @Override boolean shouldInclude(ConditionalNodeDecisionEnvironment env) { @@ -183,6 +191,27 @@ class ConditionalNodesTest extends Specification { then: er["data"] == ["in": "in", "out": "out"] + + // TODO: this test demonstrates that the GraphQLContext is missing for Pet + // and therefore, fields are being incorrectly included + when: + ei = ExecutionInput.newExecutionInput() + .graphQLContext(contextMap) + .query(""" + query q { + in + pet { + name + favouriteSnack @featureFlag(flagName : "OFF") + } + } + """ + ).build() + er = graphQL.execute(ei) + + then: + // TODO: favouriteSnack should not appear in the data + er["data"] == ["in": "in", "pet": ["name": "name"]] } private ArrayList directive(String name, Argument argument) { From 64b887dab2246bbc446d12757e6bedb4d0068a11 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 9 Jan 2024 11:23:57 +1000 Subject: [PATCH 074/393] make constructor private rename Inner to Impl --- .../ExecutableNormalizedOperationFactory.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 2f878fd3f9..36a07f4866 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.collect.ImmutableKit; @@ -47,7 +46,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.function.BiConsumer; import static graphql.Assert.assertNotNull; import static graphql.Assert.assertShouldNeverHappen; @@ -152,12 +150,7 @@ public int getMaxChildrenDepth() { } } - /** - * @deprecated the API surface of this class is all static methods, eventually this constructor will be made `private`. - */ - @Deprecated - @DeprecatedAt("2024-01-08") - public ExecutableNormalizedOperationFactory() { + private ExecutableNormalizedOperationFactory() { } @@ -205,7 +198,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( OperationDefinition operationDefinition, Map fragments, CoercedVariables coercedVariableValues) { - return new ExecutableNormalizedOperationFactoryInner( + return new ExecutableNormalizedOperationFactoryImpl( graphQLSchema, operationDefinition, fragments, @@ -300,7 +293,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW options.getGraphQLContext(), options.getLocale()); - return new ExecutableNormalizedOperationFactoryInner( + return new ExecutableNormalizedOperationFactoryImpl( graphQLSchema, operationDefinition, getOperationResult.fragmentsByName, @@ -311,7 +304,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW } - private static class ExecutableNormalizedOperationFactoryInner { + private static class ExecutableNormalizedOperationFactoryImpl { private final GraphQLSchema graphQLSchema; private final OperationDefinition operationDefinition; private final Map fragments; @@ -326,7 +319,7 @@ private static class ExecutableNormalizedOperationFactoryInner { private final ImmutableMap.Builder normalizedFieldToQueryDirectives = ImmutableMap.builder(); private final ImmutableListMultimap.Builder coordinatesToNormalizedFields = ImmutableListMultimap.builder(); - private ExecutableNormalizedOperationFactoryInner( + private ExecutableNormalizedOperationFactoryImpl( GraphQLSchema graphQLSchema, OperationDefinition operationDefinition, Map fragments, From b9b0f5497c9efba421050e58ac65db2740009b1a Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 13:03:01 +1100 Subject: [PATCH 075/393] WIP: attempt of getting rid of the NormalizedDeferExecutionFactory WIP WIP --- .../java/graphql/normalized/ENFMerger.java | 19 +++- .../normalized/ExecutableNormalizedField.java | 16 ++- .../ExecutableNormalizedOperationFactory.java | 106 ++++++++---------- ...tableNormalizedOperationToAstCompiler.java | 10 +- .../incremental/DeferDeclaration.java | 34 ++++-- .../incremental/IncrementalNodes.java | 10 +- .../NormalizedDeferExecutionFactory.java | 4 +- ...NormalizedOperationFactoryDeferTest.groovy | 56 +++++++-- 8 files changed, 160 insertions(+), 95 deletions(-) diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index d876cf56db..94d01ee739 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,11 +1,9 @@ package graphql.normalized; -import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; -import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -25,7 +23,7 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - Multimap normalizedFieldToDeferExecution + boolean deferSupport ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same @@ -74,9 +72,20 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); // Move defer executions from removed field into the merged field's entry - normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); +// normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); + if (deferSupport) { + first.getDeferExecutions().forEach(deferDeclarationFirst -> { + next.getDeferExecutions().forEach(deferDeclarationNext -> { + if (Objects.equals(deferDeclarationFirst.getLabel(), deferDeclarationNext.getLabel()) + && mergedObjects.containsAll(deferDeclarationNext.getObjectTypeNames()) + ) { + deferDeclarationFirst.addObjectTypes(deferDeclarationNext.getObjectTypes()); + } + }); + }); + } parent.getChildren().remove(next); - normalizedFieldToDeferExecution.removeAll(next); +// normalizedFieldToDeferExecution.removeAll(next); } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 93f128fa59..d27d8a970d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -11,6 +11,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; +import graphql.normalized.incremental.DeferDeclaration; import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; @@ -67,7 +68,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -263,11 +264,16 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } + @Internal + public void addDeferExecutions(Collection deferDeclarations) { + this.deferExecutions.addAll(deferDeclarations); + } + /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -475,7 +481,7 @@ public ExecutableNormalizedField getParent() { // TODO: Javadoc @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -606,7 +612,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -677,7 +683,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 0ec0891ab8..ed65c92de0 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -45,7 +45,6 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -60,7 +59,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -412,12 +410,12 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; - LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); - normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); +// LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); +// normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); - Consumer captureCollectNFResult = (collectNFResult -> { - normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); - }); +// Consumer captureCollectNFResult = (collectNFResult -> { +// normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); +// }); for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); @@ -437,17 +435,17 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), - captureCollectNFResult, +// captureCollectNFResult, options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.getDeferSupport()); } - if (options.deferSupport) { - NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); - } +// if (options.deferSupport) { +// NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); +// } return new ExecutableNormalizedOperation( operationDefinition.getOperation(), @@ -469,7 +467,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, - Consumer captureCollectNFResult, +// Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -477,7 +475,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); - captureCollectNFResult.accept(nextLevel); +// captureCollectNFResult.accept(nextLevel); for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); @@ -497,7 +495,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, - captureCollectNFResult, +// captureCollectNFResult, deferSupport); } } @@ -535,16 +533,16 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; - private final ImmutableSetMultimap normalizedFieldToDeferExecution; +// private final ImmutableSetMultimap normalizedFieldToDeferExecution; public CollectNFResult( Collection children, - ImmutableListMultimap normalizedFieldToAstFields, - ImmutableSetMultimap normalizedFieldToDeferExecution + ImmutableListMultimap normalizedFieldToAstFields +// ImmutableSetMultimap normalizedFieldToDeferExecution ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; - this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; +// this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -557,7 +555,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of());//, ImmutableSetMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -578,11 +576,11 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); +// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); +// + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build());//, normalizedFieldToDeferExecution.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -604,11 +602,11 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); +// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, @@ -623,7 +621,6 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -638,7 +635,8 @@ private void createNFs(ImmutableList.Builder nfListBu } nfListBuilder.add(nf); if (deferSupport) { - normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + nf.addDeferExecutions(fieldGroup.deferExecutions); +// normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); } } if (commonParentsGroups.size() > 1) { @@ -729,8 +727,20 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build().stream() + Set allDeferExecutions = deferExecutionsBuilder.build(); + + Set> typeSets = allDeferExecutions.stream().map(DeferDeclaration::getObjectTypeNames).collect(toSet()); + + Set deferExecutions = allDeferExecutions.stream() .filter(distinctByNullLabelAndType()) + .filter(deferDeclaration -> { + return typeSets.stream().noneMatch(typeSet -> { + boolean isSubset = typeSet.containsAll(deferDeclaration.getObjectTypeNames()); + boolean isSame = typeSet.equals(deferDeclaration.getObjectTypeNames()); + + return isSubset && !isSame; + }); + }) .collect(toCollection(LinkedHashSet::new)); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -750,7 +760,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); Set filteredDeferExecutions = deferExecutions.stream() - .filter(filter(objectType)) + .filter(deferExecution -> deferExecution.getObjectTypeNames().contains(objectType.getName())) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -758,21 +768,6 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filter(GraphQLObjectType objectType) { - return deferExecution -> { - if (deferExecution.getTargetType() == null) { - return true; - } - - if (deferExecution.getTargetType().equals(objectType.getName())) { - return true; - } - - return objectType.getInterfaces().stream() - .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); - }; - } - /** * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. @@ -796,16 +791,12 @@ private static Predicate filter(GraphQLObjectType objectType) * } * */ - private static @NotNull Predicate distinctByNullLabelAndType() { - Map seen = new ConcurrentHashMap<>(); + private static Predicate distinctByNullLabelAndType() { + Map, Boolean> seen = new ConcurrentHashMap<>(); return deferExecution -> { if (deferExecution.getLabel() == null) { - String typeName = Optional.ofNullable(deferExecution.getTargetType()) - .map(String::toUpperCase) - .orElse("null"); - - return seen.putIfAbsent(typeName, Boolean.TRUE) == null; + return seen.putIfAbsent(deferExecution.getObjectTypeNames(), Boolean.TRUE) == null; } return true; @@ -884,14 +875,15 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } + GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); + Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); + DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), fragmentSpread.getDirectives(), - fragmentDefinition.getTypeCondition() + newPossibleObjects ); - GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); - Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -916,7 +908,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), - inlineFragment.getTypeCondition() + newPossibleObjects ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index cae6f42ae7..ce7f1f4deb 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -279,7 +279,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -322,8 +322,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); + if (typeAndDeferPair.deferExecution.getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -491,9 +491,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final NormalizedDeferExecution deferExecution; + private final DeferDeclaration deferExecution; - public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, DeferDeclaration deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java index 688b9dd8a8..4db580f169 100644 --- a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java +++ b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java @@ -1,8 +1,15 @@ package graphql.normalized.incremental; +import graphql.Assert; import graphql.Internal; +import graphql.schema.GraphQLObjectType; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; /** * TODO: Javadoc @@ -10,11 +17,15 @@ @Internal public class DeferDeclaration { private final String label; - private final String targetType; - public DeferDeclaration(@Nullable String label, @Nullable String targetType) { + private final LinkedHashSet objectTypes; + + public DeferDeclaration(@Nullable String label, @Nonnull Set objectTypes) { this.label = label; - this.targetType = targetType; + + Assert.assertNotEmpty(objectTypes, () -> "A defer declaration must be associated with at least 1 GraphQL object"); + + this.objectTypes = new LinkedHashSet<>(objectTypes); } /** @@ -25,11 +36,20 @@ public String getLabel() { return label; } + public void addObjectTypes(Collection objectTypes) { + this.objectTypes.addAll(objectTypes); + } + /** - * @return the name of the type that is the target of the defer declaration + * TODO Javadoc */ - @Nullable - public String getTargetType() { - return targetType; + public Set getObjectTypes() { + return objectTypes; + } + + public Set getObjectTypeNames() { + return objectTypes.stream() + .map(GraphQLObjectType::getName) + .collect(Collectors.toSet()); } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 5b3f240d36..e433b2beeb 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -8,11 +8,13 @@ import graphql.language.Directive; import graphql.language.NodeUtil; import graphql.language.TypeName; +import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import static graphql.Directives.DeferDirective; @@ -22,7 +24,7 @@ public class IncrementalNodes { public DeferDeclaration getDeferExecution( Map variables, List directives, - @Nullable TypeName targetType + Set possibleTypes ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -38,15 +40,13 @@ public DeferDeclaration getDeferExecution( Object label = argumentValues.get("label"); - String targetTypeName = targetType == null ? null : targetType.getName(); - if (label == null) { - return new DeferDeclaration(null, targetTypeName); + return new DeferDeclaration(null, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferDeclaration((String) label, targetTypeName); + return new DeferDeclaration((String) label, possibleTypes); } diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java index 0ef085332b..59ab5e7cc2 100644 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -74,7 +74,7 @@ private void execute() { Set types = executionsForLabel.stream() .map(deferExecution -> { declarationToBlock.put(deferExecution, deferBlock); - return deferExecution.getTargetType(); + return "MOCK"; //deferExecution.getTargetType(); }) .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) .collect(Collectors.toSet()); @@ -86,7 +86,7 @@ private void execute() { if (!deferExecutions.isEmpty()) { // Mutate the field, by setting deferExecutions - field.setDeferExecutions(deferExecutions); +// field.setDeferExecutions(deferExecutions); } }); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index eb2260ee11..b46aa6318b 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -31,20 +31,26 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { name: String age: Int } + + interface HousePet implements Animal & LivingThing { + name: String + age: Int + owner: Person + } - type Dog implements Animal & LivingThing { + type Dog implements Animal & LivingThing & HousePet { name: String age: Int breed: String owner: Person } - type Cat implements Animal & LivingThing { + type Cat implements Animal & LivingThing & HousePet { name: String age: Int breed: String color: String - siblings: [Cat] + owner: Person } type Fish implements Animal & LivingThing { @@ -113,6 +119,37 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "fragment on field from interface that reduces the possible object types"() { + given: + + String query = ''' + query q { + animal { + ... on HousePet @defer { + name + owner { + firstname + } + age + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + "[Cat, Dog].name defer{[label=null;types=[Cat, Dog]]}", + "[Cat, Dog].owner defer{[label=null;types=[Cat, Dog]]}", + "Person.firstname", + "[Cat, Dog].age defer{[label=null;types=[Cat, Dog]]}" + ] + } + def "fragments on non-conditional fields"() { given: @@ -390,7 +427,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) - then: "should result in the same instance of defer" + then: "should result in different instances of defer" def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") @@ -400,8 +437,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { catBreedField.deferExecutions.size() == 1 // same label instances - nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock - dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock + nameField.deferExecutions[0] != dogBreedField.deferExecutions[0] + dogBreedField.deferExecutions[0] != catBreedField.deferExecutions[0] + nameField.deferExecutions[0] != catBreedField.deferExecutions[0] printedTree == ['Query.animal', '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', @@ -441,7 +479,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { breedField.deferExecutions.size() == 1 // different label instances - nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock + nameField.deferExecutions[0] != breedField.deferExecutions[0] printedTree == ['Query.dog', 'Dog.name defer{[label=null;types=[Dog]]}', @@ -884,8 +922,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = new ArrayList<>(deferExecutions) - .sort { it.deferBlock.label } - .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } + .sort { it.label } + .collect { "[label=${it.label};types=${it.objectTypeNames.sort()}]" } .join(",") return " defer{${deferLabels}}" From ca1f6d4d7289ee19a4e7db7ce95f4777bb6c1a4c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 14:17:28 +1100 Subject: [PATCH 076/393] Revert "WIP: attempt of getting rid of the NormalizedDeferExecutionFactory" This reverts commit b9b0f5497c9efba421050e58ac65db2740009b1a. --- .../java/graphql/normalized/ENFMerger.java | 19 +--- .../normalized/ExecutableNormalizedField.java | 16 +-- .../ExecutableNormalizedOperationFactory.java | 106 ++++++++++-------- ...tableNormalizedOperationToAstCompiler.java | 10 +- .../incremental/DeferDeclaration.java | 34 ++---- .../incremental/IncrementalNodes.java | 10 +- .../NormalizedDeferExecutionFactory.java | 4 +- ...NormalizedOperationFactoryDeferTest.groovy | 56 ++------- 8 files changed, 95 insertions(+), 160 deletions(-) diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index 94d01ee739..d876cf56db 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,9 +1,11 @@ package graphql.normalized; +import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; +import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -23,7 +25,7 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - boolean deferSupport + Multimap normalizedFieldToDeferExecution ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same @@ -72,20 +74,9 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); // Move defer executions from removed field into the merged field's entry -// normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); - if (deferSupport) { - first.getDeferExecutions().forEach(deferDeclarationFirst -> { - next.getDeferExecutions().forEach(deferDeclarationNext -> { - if (Objects.equals(deferDeclarationFirst.getLabel(), deferDeclarationNext.getLabel()) - && mergedObjects.containsAll(deferDeclarationNext.getObjectTypeNames()) - ) { - deferDeclarationFirst.addObjectTypes(deferDeclarationNext.getObjectTypes()); - } - }); - }); - } + normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); parent.getChildren().remove(next); -// normalizedFieldToDeferExecution.removeAll(next); + normalizedFieldToDeferExecution.removeAll(next); } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index d27d8a970d..93f128fa59 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -11,7 +11,6 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.DeferDeclaration; import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; @@ -68,7 +67,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -264,16 +263,11 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } - @Internal - public void addDeferExecutions(Collection deferDeclarations) { - this.deferExecutions.addAll(deferDeclarations); - } - /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -481,7 +475,7 @@ public ExecutableNormalizedField getParent() { // TODO: Javadoc @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -612,7 +606,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -683,7 +677,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index ed65c92de0..0ec0891ab8 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -45,6 +45,7 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -59,6 +60,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -410,12 +412,12 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; -// LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); -// normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); + LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); + normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); -// Consumer captureCollectNFResult = (collectNFResult -> { -// normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); -// }); + Consumer captureCollectNFResult = (collectNFResult -> { + normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); + }); for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); @@ -435,17 +437,17 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), -// captureCollectNFResult, + captureCollectNFResult, options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.getDeferSupport()); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); } -// if (options.deferSupport) { -// NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); -// } + if (options.deferSupport) { + NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); + } return new ExecutableNormalizedOperation( operationDefinition.getOperation(), @@ -467,7 +469,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, -// Consumer captureCollectNFResult, + Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -475,7 +477,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); -// captureCollectNFResult.accept(nextLevel); + captureCollectNFResult.accept(nextLevel); for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); @@ -495,7 +497,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, -// captureCollectNFResult, + captureCollectNFResult, deferSupport); } } @@ -533,16 +535,16 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; -// private final ImmutableSetMultimap normalizedFieldToDeferExecution; + private final ImmutableSetMultimap normalizedFieldToDeferExecution; public CollectNFResult( Collection children, - ImmutableListMultimap normalizedFieldToAstFields -// ImmutableSetMultimap normalizedFieldToDeferExecution + ImmutableListMultimap normalizedFieldToAstFields, + ImmutableSetMultimap normalizedFieldToDeferExecution ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; -// this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; + this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -555,7 +557,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of());//, ImmutableSetMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -576,11 +578,11 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); -// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); -// - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); + + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build());//, normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -602,11 +604,11 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); -// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, @@ -621,6 +623,7 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -635,8 +638,7 @@ private void createNFs(ImmutableList.Builder nfListBu } nfListBuilder.add(nf); if (deferSupport) { - nf.addDeferExecutions(fieldGroup.deferExecutions); -// normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); } } if (commonParentsGroups.size() > 1) { @@ -727,20 +729,8 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set allDeferExecutions = deferExecutionsBuilder.build(); - - Set> typeSets = allDeferExecutions.stream().map(DeferDeclaration::getObjectTypeNames).collect(toSet()); - - Set deferExecutions = allDeferExecutions.stream() + Set deferExecutions = deferExecutionsBuilder.build().stream() .filter(distinctByNullLabelAndType()) - .filter(deferDeclaration -> { - return typeSets.stream().noneMatch(typeSet -> { - boolean isSubset = typeSet.containsAll(deferDeclaration.getObjectTypeNames()); - boolean isSame = typeSet.equals(deferDeclaration.getObjectTypeNames()); - - return isSubset && !isSame; - }); - }) .collect(toCollection(LinkedHashSet::new)); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -760,7 +750,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); Set filteredDeferExecutions = deferExecutions.stream() - .filter(deferExecution -> deferExecution.getObjectTypeNames().contains(objectType.getName())) + .filter(filter(objectType)) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -768,6 +758,21 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } + private static Predicate filter(GraphQLObjectType objectType) { + return deferExecution -> { + if (deferExecution.getTargetType() == null) { + return true; + } + + if (deferExecution.getTargetType().equals(objectType.getName())) { + return true; + } + + return objectType.getInterfaces().stream() + .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); + }; + } + /** * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. @@ -791,12 +796,16 @@ private List groupByCommonParentsWithDeferSupport(Collectio * } * */ - private static Predicate distinctByNullLabelAndType() { - Map, Boolean> seen = new ConcurrentHashMap<>(); + private static @NotNull Predicate distinctByNullLabelAndType() { + Map seen = new ConcurrentHashMap<>(); return deferExecution -> { if (deferExecution.getLabel() == null) { - return seen.putIfAbsent(deferExecution.getObjectTypeNames(), Boolean.TRUE) == null; + String typeName = Optional.ofNullable(deferExecution.getTargetType()) + .map(String::toUpperCase) + .orElse("null"); + + return seen.putIfAbsent(typeName, Boolean.TRUE) == null; } return true; @@ -875,15 +884,14 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); - Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), fragmentSpread.getDirectives(), - newPossibleObjects + fragmentDefinition.getTypeCondition() ); + GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); + Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -908,7 +916,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), - newPossibleObjects + inlineFragment.getTypeCondition() ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index ce7f1f4deb..cae6f42ae7 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -279,7 +279,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -322,8 +322,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); + if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -491,9 +491,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferDeclaration deferExecution; + private final NormalizedDeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, DeferDeclaration deferExecution) { + public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java index 4db580f169..688b9dd8a8 100644 --- a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java +++ b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java @@ -1,15 +1,8 @@ package graphql.normalized.incremental; -import graphql.Assert; import graphql.Internal; -import graphql.schema.GraphQLObjectType; -import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.stream.Collectors; /** * TODO: Javadoc @@ -17,15 +10,11 @@ @Internal public class DeferDeclaration { private final String label; + private final String targetType; - private final LinkedHashSet objectTypes; - - public DeferDeclaration(@Nullable String label, @Nonnull Set objectTypes) { + public DeferDeclaration(@Nullable String label, @Nullable String targetType) { this.label = label; - - Assert.assertNotEmpty(objectTypes, () -> "A defer declaration must be associated with at least 1 GraphQL object"); - - this.objectTypes = new LinkedHashSet<>(objectTypes); + this.targetType = targetType; } /** @@ -36,20 +25,11 @@ public String getLabel() { return label; } - public void addObjectTypes(Collection objectTypes) { - this.objectTypes.addAll(objectTypes); - } - /** - * TODO Javadoc + * @return the name of the type that is the target of the defer declaration */ - public Set getObjectTypes() { - return objectTypes; - } - - public Set getObjectTypeNames() { - return objectTypes.stream() - .map(GraphQLObjectType::getName) - .collect(Collectors.toSet()); + @Nullable + public String getTargetType() { + return targetType; } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index e433b2beeb..5b3f240d36 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -8,13 +8,11 @@ import graphql.language.Directive; import graphql.language.NodeUtil; import graphql.language.TypeName; -import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import static graphql.Directives.DeferDirective; @@ -24,7 +22,7 @@ public class IncrementalNodes { public DeferDeclaration getDeferExecution( Map variables, List directives, - Set possibleTypes + @Nullable TypeName targetType ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -40,13 +38,15 @@ public DeferDeclaration getDeferExecution( Object label = argumentValues.get("label"); + String targetTypeName = targetType == null ? null : targetType.getName(); + if (label == null) { - return new DeferDeclaration(null, possibleTypes); + return new DeferDeclaration(null, targetTypeName); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferDeclaration((String) label, possibleTypes); + return new DeferDeclaration((String) label, targetTypeName); } diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java index 59ab5e7cc2..0ef085332b 100644 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -74,7 +74,7 @@ private void execute() { Set types = executionsForLabel.stream() .map(deferExecution -> { declarationToBlock.put(deferExecution, deferBlock); - return "MOCK"; //deferExecution.getTargetType(); + return deferExecution.getTargetType(); }) .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) .collect(Collectors.toSet()); @@ -86,7 +86,7 @@ private void execute() { if (!deferExecutions.isEmpty()) { // Mutate the field, by setting deferExecutions -// field.setDeferExecutions(deferExecutions); + field.setDeferExecutions(deferExecutions); } }); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index b46aa6318b..eb2260ee11 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -31,26 +31,20 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { name: String age: Int } - - interface HousePet implements Animal & LivingThing { - name: String - age: Int - owner: Person - } - type Dog implements Animal & LivingThing & HousePet { + type Dog implements Animal & LivingThing { name: String age: Int breed: String owner: Person } - type Cat implements Animal & LivingThing & HousePet { + type Cat implements Animal & LivingThing { name: String age: Int breed: String color: String - owner: Person + siblings: [Cat] } type Fish implements Animal & LivingThing { @@ -119,37 +113,6 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "fragment on field from interface that reduces the possible object types"() { - given: - - String query = ''' - query q { - animal { - ... on HousePet @defer { - name - owner { - firstname - } - age - } - } - } - ''' - - Map variables = [:] - - when: - List printedTree = executeQueryAndPrintTree(query, variables) - - then: - printedTree == ['Query.animal', - "[Cat, Dog].name defer{[label=null;types=[Cat, Dog]]}", - "[Cat, Dog].owner defer{[label=null;types=[Cat, Dog]]}", - "Person.firstname", - "[Cat, Dog].age defer{[label=null;types=[Cat, Dog]]}" - ] - } - def "fragments on non-conditional fields"() { given: @@ -427,7 +390,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) - then: "should result in different instances of defer" + then: "should result in the same instance of defer" def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") @@ -437,9 +400,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { catBreedField.deferExecutions.size() == 1 // same label instances - nameField.deferExecutions[0] != dogBreedField.deferExecutions[0] - dogBreedField.deferExecutions[0] != catBreedField.deferExecutions[0] - nameField.deferExecutions[0] != catBreedField.deferExecutions[0] + nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock + dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock printedTree == ['Query.animal', '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', @@ -479,7 +441,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { breedField.deferExecutions.size() == 1 // different label instances - nameField.deferExecutions[0] != breedField.deferExecutions[0] + nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock printedTree == ['Query.dog', 'Dog.name defer{[label=null;types=[Dog]]}', @@ -922,8 +884,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = new ArrayList<>(deferExecutions) - .sort { it.label } - .collect { "[label=${it.label};types=${it.objectTypeNames.sort()}]" } + .sort { it.deferBlock.label } + .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } .join(",") return " defer{${deferLabels}}" From 6ba3e09f34d6b14a43e1418607728f299c31aefc Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 15:22:13 +1100 Subject: [PATCH 077/393] Remove excessive normalization that was resulting in reduced number of defer blocks --- .../java/graphql/normalized/ENFMerger.java | 10 ++- .../ExecutableNormalizedOperationFactory.java | 54 ++------------- .../NormalizedDeferExecutionFactory.java | 34 ++++----- ...NormalizedOperationFactoryDeferTest.groovy | 69 ++++++++++++++++--- ...izedOperationToAstCompilerDeferTest.groovy | 9 +++ 5 files changed, 95 insertions(+), 81 deletions(-) diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index d876cf56db..69d5faaefa 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -25,7 +25,8 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - Multimap normalizedFieldToDeferExecution + Multimap normalizedFieldToDeferExecution, + boolean deferSupport ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same @@ -74,9 +75,12 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); // Move defer executions from removed field into the merged field's entry - normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); parent.getChildren().remove(next); - normalizedFieldToDeferExecution.removeAll(next); + + if (deferSupport) { + normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); + normalizedFieldToDeferExecution.removeAll(next); + } } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 0ec0891ab8..754e6a5904 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -45,7 +45,6 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -56,9 +55,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -415,9 +412,9 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); - Consumer captureCollectNFResult = (collectNFResult -> { - normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); - }); + Consumer captureCollectNFResult = (collectNFResult -> + normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution) + ); for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); @@ -442,7 +439,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution, options.deferSupport); } if (options.deferSupport) { @@ -729,9 +726,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build().stream() - .filter(distinctByNullLabelAndType()) - .collect(toCollection(LinkedHashSet::new)); + Set deferExecutions = deferExecutionsBuilder.build(); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -773,45 +768,6 @@ private static Predicate filter(GraphQLObjectType objectType) }; } - /** - * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. - * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. - * For example, this query: - *

-     *     query example {
-     *        ... @defer {
-     *            name
-     *        }
-     *        ... @defer {
-     *            name
-     *        }
-     *     }
-     * 
- * should result on single ENF. Essentially: - *
-     *     query example {
-     *        ... @defer {
-     *            name
-     *        }
-     *     }
-     * 
- */ - private static @NotNull Predicate distinctByNullLabelAndType() { - Map seen = new ConcurrentHashMap<>(); - - return deferExecution -> { - if (deferExecution.getLabel() == null) { - String typeName = Optional.ofNullable(deferExecution.getTargetType()) - .map(String::toUpperCase) - .orElse("null"); - - return seen.putIfAbsent(typeName, Boolean.TRUE) == null; - } - - return true; - }; - } - private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() .map(DeferDeclaration::getLabel) diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java index 0ef085332b..ffa17e8cca 100644 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -14,7 +14,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -25,18 +24,20 @@ // TODO: Javadoc @Internal public class NormalizedDeferExecutionFactory { + private NormalizedDeferExecutionFactory() { + } public static void normalizeDeferExecutions( GraphQLSchema graphQLSchema, Multimap normalizedFieldDeferExecution ) { - new DeferExecutionMergerInner(graphQLSchema, normalizedFieldDeferExecution).execute(); + new DeferExecutionMergerImpl(graphQLSchema, normalizedFieldDeferExecution).execute(); } - private static class DeferExecutionMergerInner { + private static class DeferExecutionMergerImpl { private final GraphQLSchema graphQLSchema; private final Multimap input; - private DeferExecutionMergerInner( + private DeferExecutionMergerImpl( GraphQLSchema graphQLSchema, Multimap normalizedFieldToDeferExecution ) { @@ -62,25 +63,20 @@ private void execute() { .collect(Collectors.groupingBy(execution -> Optional.ofNullable(execution.getLabel()))); Set deferExecutions = executionsByLabel.keySet().stream() - .map(label -> { + .flatMap(label -> { List executionsForLabel = executionsByLabel.get(label); - DeferBlock deferBlock = executionsForLabel.stream() - .map(declarationToBlock::get) - .filter(Objects::nonNull) - .findFirst() - .orElse(new DeferBlock(label.orElse(null))); - - Set types = executionsForLabel.stream() - .map(deferExecution -> { - declarationToBlock.put(deferExecution, deferBlock); - return deferExecution.getTargetType(); - }) - .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) - .collect(Collectors.toSet()); + return executionsForLabel.stream() + .map(deferDeclaration -> { + DeferBlock deferBlock = declarationToBlock.computeIfAbsent(deferDeclaration, + key -> new DeferBlock(label.orElse(null))); - return new NormalizedDeferExecution(deferBlock, types); + Set types = mapToPossibleTypes(fieldTypeNames, fieldTypes) + .apply(deferDeclaration.getTargetType()) + .collect(Collectors.toSet()); + return new NormalizedDeferExecution(deferBlock, types); + }); }) .collect(Collectors.toSet()); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index eb2260ee11..0b3a1d29ea 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -139,7 +139,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}", + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]],[label=null;types=[Cat, Dog, Fish]]}", ] } @@ -169,7 +169,46 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog]]}", + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]]}", + ] + } + + def "fragments on non-conditional fields - andi"() { + given: + + + String query = ''' + query q { + dog { + ... @defer { + name + age + } + ... @defer { + age + } + } + } + ''' +// This should result in age being on its own deferBlock + Map variables = [:] + + when: + def executableNormalizedOperation = createExecutableNormalizedOperations(query, variables); + + List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) + + then: + + def nameField = findField(executableNormalizedOperation, "Dog", "name") + def ageField = findField(executableNormalizedOperation, "Dog", "age") + + nameField.deferExecutions.size() == 1 + ageField.deferExecutions.size() == 2 + + printedTree == ['Query.dog', + "Dog.name defer{[label=null;types=[Dog]]}", + "Dog.age defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}", ] } @@ -246,7 +285,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.mammal', - '[Dog, Cat].name defer{[label=null;types=[Cat, Dog]]}', + '[Dog, Cat].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]]}', 'Dog.breed defer{[label=null;types=[Dog]]}', 'Cat.breed defer{[label=null;types=[Cat]]}', ] @@ -390,21 +429,30 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) - then: "should result in the same instance of defer" + then: "should result in the same instance of defer block" def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") - nameField.deferExecutions.size() == 1 + nameField.deferExecutions.size() == 3 dogBreedField.deferExecutions.size() == 1 catBreedField.deferExecutions.size() == 1 - // same label instances - nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock - dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock + // nameField should share a defer block with each of the other fields + nameField.deferExecutions.any { + it.deferBlock == dogBreedField.deferExecutions[0].deferBlock + } + nameField.deferExecutions.any { + it.deferBlock == catBreedField.deferExecutions[0].deferBlock + } + // also, nameField should have a defer block that is not shared with any other field + nameField.deferExecutions.any { + it.deferBlock != dogBreedField.deferExecutions[0].deferBlock && + it.deferBlock != catBreedField.deferExecutions[0].deferBlock + } printedTree == ['Query.animal', - '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]],[label=null;types=[Cat, Dog, Fish]]}', 'Dog.breed defer{[label=null;types=[Dog]]}', 'Cat.breed defer{[label=null;types=[Cat]]}', ] @@ -589,7 +637,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer{[label=null;types=[Dog]]}', + 'Dog.name defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}', ] } @@ -885,6 +933,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { def deferLabels = new ArrayList<>(deferExecutions) .sort { it.deferBlock.label } + .sort { it.objectTypeNames } .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } .join(",") diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 8d7f9e91a3..13928fd991 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -177,6 +177,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { breed } + ... @defer { + breed + } } } ''' @@ -277,6 +280,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { name } + ... @defer { + name + } } } ''' @@ -421,6 +427,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { + ... @defer { + name + } ... @defer { name } From b71a9a41d1f04d58bd46a30f712adb526d13054b Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 16:16:52 +1100 Subject: [PATCH 078/393] Big refactor: capture defer executions directly in ENFs --- .../java/graphql/normalized/ENFMerger.java | 12 +- .../normalized/ExecutableNormalizedField.java | 17 +-- .../ExecutableNormalizedOperationFactory.java | 87 +++++-------- ...tableNormalizedOperationToAstCompiler.java | 14 +-- ...erDeclaration.java => DeferExecution.java} | 15 ++- .../incremental/IncrementalNodes.java | 12 +- .../incremental/NormalizedDeferExecution.java | 43 ------- .../NormalizedDeferExecutionFactory.java | 115 ------------------ ...NormalizedOperationFactoryDeferTest.groovy | 16 +-- 9 files changed, 80 insertions(+), 251 deletions(-) rename src/main/java/graphql/normalized/incremental/{DeferDeclaration.java => DeferExecution.java} (58%) delete mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java delete mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index 69d5faaefa..f1fbc37b95 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,11 +1,9 @@ package graphql.normalized; -import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; -import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -25,7 +23,6 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - Multimap normalizedFieldToDeferExecution, boolean deferSupport ) { // they have all the same result key @@ -66,20 +63,19 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) } boolean mergeable = areFieldSetsTheSame(listOfChildrenForGroup); if (mergeable) { + Set mergedObjects = new LinkedHashSet<>(); + groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); // patching the first one to contain more objects, remove all others Iterator iterator = groupOfFields.iterator(); ExecutableNormalizedField first = iterator.next(); - Set mergedObjects = new LinkedHashSet<>(); - groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); - // Move defer executions from removed field into the merged field's entry parent.getChildren().remove(next); if (deferSupport) { - normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); - normalizedFieldToDeferExecution.removeAll(next); + // Move defer executions from removed field into the merged field's entry + first.addDeferExecutions(next.getDeferExecutions()); } } first.setObjectTypeNames(mergedObjects); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 93f128fa59..0dfbd9a892 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import graphql.Assert; import graphql.ExperimentalApi; import graphql.Internal; @@ -11,7 +10,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.NormalizedDeferExecution; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -67,7 +66,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -263,11 +262,15 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } + public void addDeferExecutions(Collection deferExecutions) { + this.deferExecutions.addAll(deferExecutions); + } + /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -475,7 +478,7 @@ public ExecutableNormalizedField getParent() { // TODO: Javadoc @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -606,7 +609,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -677,7 +680,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 754e6a5904..eba246ede9 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,8 +4,6 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.LinkedHashMultimap; import graphql.Assert; import graphql.ExperimentalApi; import graphql.GraphQLContext; @@ -30,9 +28,8 @@ import graphql.language.Selection; import graphql.language.SelectionSet; import graphql.language.VariableDefinition; -import graphql.normalized.incremental.DeferDeclaration; +import graphql.normalized.incremental.DeferExecution; import graphql.normalized.incremental.IncrementalNodes; -import graphql.normalized.incremental.NormalizedDeferExecutionFactory; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -57,7 +54,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -409,13 +405,6 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; - LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); - normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); - - Consumer captureCollectNFResult = (collectNFResult -> - normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution) - ); - for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); MergedField mergedField = newMergedField(fieldAndAstParents); @@ -434,16 +423,11 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), - captureCollectNFResult, options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution, options.deferSupport); - } - - if (options.deferSupport) { - NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.deferSupport); } return new ExecutableNormalizedOperation( @@ -466,7 +450,6 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, - Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -474,8 +457,6 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); - captureCollectNFResult.accept(nextLevel); - for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); @@ -494,7 +475,6 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, - captureCollectNFResult, deferSupport); } } @@ -532,16 +512,13 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; - private final ImmutableSetMultimap normalizedFieldToDeferExecution; public CollectNFResult( Collection children, - ImmutableListMultimap normalizedFieldToAstFields, - ImmutableSetMultimap normalizedFieldToDeferExecution + ImmutableListMultimap normalizedFieldToAstFields ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; - this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -554,7 +531,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -575,11 +552,10 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -601,11 +577,10 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, @@ -620,7 +595,6 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -635,7 +609,7 @@ private void createNFs(ImmutableList.Builder nfListBu } nfListBuilder.add(nf); if (deferSupport) { - normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + nf.addDeferExecutions(fieldGroup.deferExecutions); } } if (commonParentsGroups.size() > 1) { @@ -676,9 +650,9 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p private static class CollectedFieldGroup { Set objectTypes; Set fields; - Set deferExecutions; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; this.deferExecutions = deferExecutions; @@ -713,12 +687,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferDeclaration collectedDeferExecution = collectedField.deferExecution; + DeferExecution collectedDeferExecution = collectedField.deferExecution; if (collectedDeferExecution != null) { deferExecutionsBuilder.add(collectedDeferExecution); @@ -726,7 +700,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build(); + Set deferExecutions = deferExecutionsBuilder.build(); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -744,7 +718,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - Set filteredDeferExecutions = deferExecutions.stream() + Set filteredDeferExecutions = deferExecutions.stream() .filter(filter(objectType)) .collect(toCollection(LinkedHashSet::new)); @@ -753,7 +727,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filter(GraphQLObjectType objectType) { + private static Predicate filter(GraphQLObjectType objectType) { return deferExecution -> { if (deferExecution.getTargetType() == null) { return true; @@ -768,9 +742,9 @@ private static Predicate filter(GraphQLObjectType objectType) }; } - private Set listDuplicatedLabels(Collection deferExecutions) { + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() - .map(DeferDeclaration::getLabel) + .map(DeferExecution::getLabel) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() @@ -785,7 +759,7 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferDeclaration deferExecution + DeferExecution deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { @@ -802,9 +776,9 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; - DeferDeclaration deferExecution; + DeferExecution deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferDeclaration deferExecution) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; @@ -840,14 +814,16 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( - parameters.getCoercedVariableValues(), - fragmentSpread.getDirectives(), - fragmentDefinition.getTypeCondition() - ); GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); + + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + parameters.getCoercedVariableValues(), + fragmentSpread.getDirectives(), + fragmentDefinition.getTypeCondition(), + newPossibleObjects); + collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -869,10 +845,11 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); } - DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), - inlineFragment.getTypeCondition() + inlineFragment.getTypeCondition(), + newPossibleObjects ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); @@ -883,7 +860,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferDeclaration deferExecution + DeferExecution deferExecution ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index cae6f42ae7..051d4655de 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,8 +23,7 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferDeclaration; -import graphql.normalized.incremental.NormalizedDeferExecution; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -34,7 +33,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -279,7 +277,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -322,8 +320,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); + if (typeAndDeferPair.deferExecution.getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -491,9 +489,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final NormalizedDeferExecution deferExecution; + private final DeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java similarity index 58% rename from src/main/java/graphql/normalized/incremental/DeferDeclaration.java rename to src/main/java/graphql/normalized/incremental/DeferExecution.java index 688b9dd8a8..f4bab2634f 100644 --- a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -1,20 +1,24 @@ package graphql.normalized.incremental; import graphql.Internal; +import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; +import java.util.Set; /** * TODO: Javadoc */ @Internal -public class DeferDeclaration { +public class DeferExecution { private final String label; private final String targetType; + private final Set possibleTypes; - public DeferDeclaration(@Nullable String label, @Nullable String targetType) { + public DeferExecution(@Nullable String label, @Nullable String targetType, Set possibleTypes) { this.label = label; this.targetType = targetType; + this.possibleTypes = possibleTypes; } /** @@ -32,4 +36,11 @@ public String getLabel() { public String getTargetType() { return targetType; } + + /** + * TODO Javadoc + */ + public Set getPossibleTypes() { + return possibleTypes; + } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 5b3f240d36..47199f62a8 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -8,21 +8,24 @@ import graphql.language.Directive; import graphql.language.NodeUtil; import graphql.language.TypeName; +import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import static graphql.Directives.DeferDirective; @Internal public class IncrementalNodes { - public DeferDeclaration getDeferExecution( + public DeferExecution getDeferExecution( Map variables, List directives, - @Nullable TypeName targetType + @Nullable TypeName targetType, + Set possibleTypes ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -41,13 +44,12 @@ public DeferDeclaration getDeferExecution( String targetTypeName = targetType == null ? null : targetType.getName(); if (label == null) { - return new DeferDeclaration(null, targetTypeName); + return new DeferExecution(null, targetTypeName, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferDeclaration((String) label, targetTypeName); - + return new DeferExecution((String) label, targetTypeName, possibleTypes); } return null; diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java deleted file mode 100644 index aad493018b..0000000000 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java +++ /dev/null @@ -1,43 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.ExperimentalApi; - -import javax.annotation.Nullable; -import java.util.Set; - -/** - * Represents the aspects of defer that are important for runtime execution. - */ -@ExperimentalApi -public class NormalizedDeferExecution { - private final DeferBlock deferBlock; - private final Set objectTypeNames; - - public NormalizedDeferExecution(DeferBlock deferBlock, Set objectTypeNames) { - this.deferBlock = deferBlock; - this.objectTypeNames = objectTypeNames; - } - - public Set getObjectTypeNames() { - return objectTypeNames; - } - - public DeferBlock getDeferBlock() { - return deferBlock; - } - - // TODO: Javadoc - @ExperimentalApi - public static class DeferBlock { - private final String label; - - public DeferBlock(@Nullable String label) { - this.label = label; - } - - @Nullable - public String getLabel() { - return label; - } - } -} diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java deleted file mode 100644 index ffa17e8cca..0000000000 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ /dev/null @@ -1,115 +0,0 @@ -package graphql.normalized.incremental; - -import com.google.common.collect.Multimap; -import graphql.Internal; -import graphql.normalized.ExecutableNormalizedField; -import graphql.normalized.incremental.NormalizedDeferExecution.DeferBlock; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLSchema; -import graphql.schema.GraphQLType; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -// TODO: Javadoc -@Internal -public class NormalizedDeferExecutionFactory { - private NormalizedDeferExecutionFactory() { - } - public static void normalizeDeferExecutions( - GraphQLSchema graphQLSchema, - Multimap normalizedFieldDeferExecution - ) { - new DeferExecutionMergerImpl(graphQLSchema, normalizedFieldDeferExecution).execute(); - } - - private static class DeferExecutionMergerImpl { - private final GraphQLSchema graphQLSchema; - private final Multimap input; - - private DeferExecutionMergerImpl( - GraphQLSchema graphQLSchema, - Multimap normalizedFieldToDeferExecution - ) { - this.graphQLSchema = graphQLSchema; - this.input = normalizedFieldToDeferExecution; - } - - private void execute() { - Map declarationToBlock = new HashMap<>(); - - this.input.keySet().forEach(field -> { - Collection executionsForField = input.get(field); - - Set fieldTypes = field.getObjectTypeNames().stream() - .map(graphQLSchema::getType) - .filter(GraphQLObjectType.class::isInstance) - .map(GraphQLObjectType.class::cast) - .collect(Collectors.toSet()); - - Set fieldTypeNames = fieldTypes.stream().map(GraphQLObjectType::getName).collect(Collectors.toSet()); - - Map, List> executionsByLabel = executionsForField.stream() - .collect(Collectors.groupingBy(execution -> Optional.ofNullable(execution.getLabel()))); - - Set deferExecutions = executionsByLabel.keySet().stream() - .flatMap(label -> { - List executionsForLabel = executionsByLabel.get(label); - - return executionsForLabel.stream() - .map(deferDeclaration -> { - DeferBlock deferBlock = declarationToBlock.computeIfAbsent(deferDeclaration, - key -> new DeferBlock(label.orElse(null))); - - Set types = mapToPossibleTypes(fieldTypeNames, fieldTypes) - .apply(deferDeclaration.getTargetType()) - .collect(Collectors.toSet()); - - return new NormalizedDeferExecution(deferBlock, types); - }); - }) - .collect(Collectors.toSet()); - - if (!deferExecutions.isEmpty()) { - // Mutate the field, by setting deferExecutions - field.setDeferExecutions(deferExecutions); - } - }); - } - - @NotNull - private Function> mapToPossibleTypes(Set fieldTypeNames, Set fieldTypes) { - return typeName -> { - if (typeName == null) { - return fieldTypeNames.stream(); - } - - GraphQLType type = graphQLSchema.getType(typeName); - - if (type instanceof GraphQLInterfaceType) { - return fieldTypes.stream() - .filter(filterImplementsInterface((GraphQLInterfaceType) type)) - .map(GraphQLObjectType::getName); - } - - return Stream.of(typeName); - }; - } - - private static Predicate filterImplementsInterface(GraphQLInterfaceType interfaceType) { - return objectType -> objectType.getInterfaces().stream() - .anyMatch(implementedInterface -> implementedInterface.getName().equals(interfaceType.getName())); - } - } - -} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 0b3a1d29ea..95403c86d8 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -440,15 +440,15 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { // nameField should share a defer block with each of the other fields nameField.deferExecutions.any { - it.deferBlock == dogBreedField.deferExecutions[0].deferBlock + it == dogBreedField.deferExecutions[0] } nameField.deferExecutions.any { - it.deferBlock == catBreedField.deferExecutions[0].deferBlock + it == catBreedField.deferExecutions[0] } // also, nameField should have a defer block that is not shared with any other field nameField.deferExecutions.any { - it.deferBlock != dogBreedField.deferExecutions[0].deferBlock && - it.deferBlock != catBreedField.deferExecutions[0].deferBlock + it != dogBreedField.deferExecutions[0] && + it != catBreedField.deferExecutions[0] } printedTree == ['Query.animal', @@ -489,7 +489,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { breedField.deferExecutions.size() == 1 // different label instances - nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock + nameField.deferExecutions[0] != breedField.deferExecutions[0] printedTree == ['Query.dog', 'Dog.name defer{[label=null;types=[Dog]]}', @@ -932,9 +932,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = new ArrayList<>(deferExecutions) - .sort { it.deferBlock.label } - .sort { it.objectTypeNames } - .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } + .sort { it.label } + .sort { it.possibleTypes.collect {it.name} } + .collect { "[label=${it.label};types=${it.possibleTypes.collect{it.name}.sort()}]" } .join(",") return " defer{${deferLabels}}" From eda84d3ed4c65b263fb7e51dfa2cdf1ad3273265 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:28:13 +1100 Subject: [PATCH 079/393] post merge adjustments --- .../ExecutableNormalizedOperationFactory.java | 49 ++++++++-------- ...tableNormalizedOperationFactoryTest.groovy | 56 +++++++++---------- ...ormalizedOperationToAstCompilerTest.groovy | 2 +- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 86b074aba4..651cb22e20 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -38,6 +38,7 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; @@ -52,7 +53,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -242,14 +242,14 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( ) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - return createExecutableNormalizedOperation( + return new ExecutableNormalizedOperationFactoryImpl( graphQLSchema, getOperationResult.operationDefinition, getOperationResult.fragmentsByName, coercedVariableValues, null, options - ); + ).createNormalizedQueryImpl(); } /** @@ -425,8 +425,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { topLevel, fieldAndAstParents, 1, - options.getMaxChildrenDepth(), - options.deferSupport); + options.getMaxChildrenDepth()); } // getPossibleMergerList for (PossibleMerger possibleMerger : possibleMergerList) { @@ -444,7 +443,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { ); } - private void captureMergedField(ExecutableNormalizedField enf, MergedField mergedFld) { + private void captureMergedField(ExecutableNormalizedField enf, MergedField mergedFld) { // QueryDirectivesImpl is a lazy object and only computes itself when asked for QueryDirectives queryDirectives = new QueryDirectivesImpl(mergedFld, graphQLSchema, coercedVariableValues.toMap(), options.getGraphQLContext(), options.getLocale()); normalizedFieldToQueryDirectives.put(enf, queryDirectives); @@ -454,8 +453,7 @@ private void captureMergedField(ExecutableNormalizedField enf, MergedField merge private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, ImmutableList fieldAndAstParents, int curLevel, - int maxLevel, - boolean deferSupport) { + int maxLevel) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); } @@ -475,8 +473,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz buildFieldWithChildren(childENF, childFieldAndAstParents, curLevel + 1, - maxLevel, - deferSupport); + maxLevel); } } @@ -517,7 +514,8 @@ public CollectNFResult collectFromMergedField(ExecutableNormalizedField executab this.collectFromSelectionSet(fieldAndAstParent.field.getSelectionSet(), collectedFields, (GraphQLCompositeType) astParentType, - possibleObjects + possibleObjects, + null ); } Map> fieldsByName = fieldsByResultKey(collectedFields); @@ -542,7 +540,7 @@ public CollectNFResult collectFromOperation(GraphQLObjectType rootType) { Set possibleObjects = ImmutableSet.of(rootType); List collectedFields = new ArrayList<>(); - collectFromSelectionSet(operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects); + collectFromSelectionSet(operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects, null); // group by result key Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); @@ -553,12 +551,6 @@ public CollectNFResult collectFromOperation(GraphQLObjectType rootType) { return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } - public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, - OperationDefinition operationDefinition, - GraphQLObjectType rootType) { - return this.collectFromOperation(parameters, operationDefinition, rootType, false); - } - private void createNFs(ImmutableList.Builder nfListBuilder, Map> fieldsByName, ImmutableListMultimap.Builder normalizedFieldToAstFields, @@ -626,10 +618,9 @@ public CollectedFieldGroup(Set fields, Set ob this.deferExecutions = deferExecutions; } } - } - private List groupByCommonParents(Collection fields, boolean deferSupport) { - if (deferSupport) { + private List groupByCommonParents(Collection fields) { + if (this.options.deferSupport) { return groupByCommonParentsWithDeferSupport(fields); } else { return groupByCommonParentsNoDeferSupport(fields); @@ -644,12 +635,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< Set allRelevantObjects = objectTypes.build(); Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects)); + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, null)); } ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType))); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), null)); } return result.build(); } @@ -868,11 +859,21 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; + DeferExecution deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; + this.deferExecution = deferExecution; + } + + public boolean isAbstract() { + return GraphQLTypeUtil.isInterfaceOrUnion(astTypeCondition); + } + + public boolean isConcrete() { + return GraphQLTypeUtil.isObjectType(astTypeCondition); } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 425542aab1..6063e7e448 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -329,7 +329,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -372,7 +372,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -422,7 +422,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -485,7 +485,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -531,7 +531,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -575,7 +575,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -619,7 +619,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -651,7 +651,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -702,7 +702,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -752,7 +752,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -791,7 +791,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -835,7 +835,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -875,7 +875,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -923,7 +923,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1026,7 +1026,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def subFooField = (document.getDefinitions()[1] as FragmentDefinition).getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1069,7 +1069,7 @@ type Dog implements Animal{ def petsField = (document.getDefinitions()[0] as OperationDefinition).getSelectionSet().getSelections()[0] as Field def idField = petsField.getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1118,7 +1118,7 @@ type Dog implements Animal{ def schemaField = selections[2] as Field def typeField = selections[3] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1175,7 +1175,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1218,7 +1218,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -1246,7 +1246,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def normalizedFieldToMergedField = tree.getNormalizedFieldToMergedField() Traverser traverser = Traverser.depthFirst({ it.getChildren() }) @@ -1385,7 +1385,7 @@ schema { Document document = TestUtil.parseQuery(mutation) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2584,7 +2584,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2637,7 +2637,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2684,7 +2684,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2882,10 +2882,10 @@ fragment personName on Person { String operationName, CoercedVariables coercedVariableValues ) { - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) } private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( @@ -2894,10 +2894,10 @@ fragment personName on Person { String operationName, RawVariables rawVariables ) { - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, operationName, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 6c6b3e3b6b..27c4c89a6d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2139,7 +2139,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat v3: [arg1: "barFragArg"], v4: [arg1: "barArg"], v5: [arg1: "fooArg"]] - + } private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { assertValidQuery(schema, query, variables) From 87a5b52175fb0459fd590207557bfe2bf4ac961c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:31:23 +1100 Subject: [PATCH 080/393] Remove irrelevant parameter --- .../ExecutableNormalizedOperationFactory.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 651cb22e20..cae62d5198 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -424,8 +424,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { buildFieldWithChildren( topLevel, fieldAndAstParents, - 1, - options.getMaxChildrenDepth()); + 1); } // getPossibleMergerList for (PossibleMerger possibleMerger : possibleMergerList) { @@ -452,10 +451,9 @@ private void captureMergedField(ExecutableNormalizedField enf, MergedField merge private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, ImmutableList fieldAndAstParents, - int curLevel, - int maxLevel) { - if (curLevel > maxLevel) { - throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); + int curLevel) { + if (curLevel > this.options.getMaxChildrenDepth()) { + throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + this.options.getMaxChildrenDepth()); } CollectNFResult nextLevel = collectFromMergedField(executableNormalizedField, fieldAndAstParents, curLevel + 1); @@ -472,8 +470,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz buildFieldWithChildren(childENF, childFieldAndAstParents, - curLevel + 1, - maxLevel); + curLevel + 1); } } From 5943b7c067fa58e66956b57e4f32d376163befb0 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:40:01 +1100 Subject: [PATCH 081/393] Minor refactors --- .../ExecutableNormalizedOperationFactory.java | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index cae62d5198..9eafaada50 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -18,6 +18,7 @@ import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; import graphql.introspection.Introspection; +import graphql.language.Directive; import graphql.language.Document; import graphql.language.Field; import graphql.language.FragmentDefinition; @@ -27,6 +28,7 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; +import graphql.language.TypeName; import graphql.language.VariableDefinition; import graphql.normalized.incremental.DeferExecution; import graphql.normalized.incremental.IncrementalNodes; @@ -604,18 +606,6 @@ private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGro .build(); } - private static class CollectedFieldGroup { - Set objectTypes; - Set fields; - Set deferExecutions; - - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { - this.fields = fields; - this.objectTypes = objectTypes; - this.deferExecutions = deferExecutions; - } - } - private List groupByCommonParents(Collection fields) { if (this.options.deferSupport) { return groupByCommonParentsWithDeferSupport(fields); @@ -749,8 +739,7 @@ private void collectFragmentSpread(List result, GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(this.graphQLSchema.getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition); - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( - this.coercedVariableValues.toMap(), + DeferExecution newDeferExecution = buildDeferExecution( fragmentSpread.getDirectives(), fragmentDefinition.getTypeCondition(), newPossibleObjects); @@ -775,8 +764,7 @@ private void collectInlineFragment(List result, } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( - this.coercedVariableValues.toMap(), + DeferExecution newDeferExecution = buildDeferExecution( inlineFragment.getDirectives(), inlineFragment.getTypeCondition(), newPossibleObjects @@ -785,6 +773,22 @@ private void collectInlineFragment(List result, collectFromSelectionSet(inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } + private DeferExecution buildDeferExecution( + List directives, + TypeName typeCondition, + Set newPossibleObjects) { + if(!options.deferSupport) { + return null; + } + + return incrementalNodes.getDeferExecution( + this.coercedVariableValues.toMap(), + directives, + typeCondition, + newPossibleObjects + ); + } + private void collectField(List result, Field field, Set possibleObjectTypes, @@ -893,6 +897,18 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { this.astParentType = astParentType; } } + + private static class CollectedFieldGroup { + Set objectTypes; + Set fields; + Set deferExecutions; + + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + this.fields = fields; + this.objectTypes = objectTypes; + this.deferExecutions = deferExecutions; + } + } } } From b04eadc32ab956cb8bcd3669c9f2eaa15e9e7848 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:48:14 +1100 Subject: [PATCH 082/393] Rename predicate function --- .../ExecutableNormalizedOperationFactory.java | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 9eafaada50..7116693d29 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -40,7 +40,6 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; @@ -113,7 +112,7 @@ public static Options defaultOptions() { * @return new options object to use */ public Options locale(Locale locale) { - return new Options(this.graphQLContext, locale, this.maxChildrenDepth, true); + return new Options(this.graphQLContext, locale, this.maxChildrenDepth, this.deferSupport); } /** @@ -126,7 +125,7 @@ public Options locale(Locale locale) { * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { - return new Options(graphQLContext, this.locale, this.maxChildrenDepth, true); + return new Options(graphQLContext, this.locale, this.maxChildrenDepth, this.deferSupport); } /** @@ -138,7 +137,7 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { - return new Options(this.graphQLContext, this.locale, maxChildrenDepth, true); + return new Options(this.graphQLContext, this.locale, maxChildrenDepth, this.deferSupport); } /** @@ -666,7 +665,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); Set filteredDeferExecutions = deferExecutions.stream() - .filter(filter(objectType)) + .filter(filterExecutionsFromType(objectType)) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -674,7 +673,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filter(GraphQLObjectType objectType) { + private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { return deferExecution -> { if (deferExecution.getTargetType() == null) { return true; @@ -868,14 +867,6 @@ public CollectedField(Field field, Set objectTypes, GraphQLCo this.astTypeCondition = astTypeCondition; this.deferExecution = deferExecution; } - - public boolean isAbstract() { - return GraphQLTypeUtil.isInterfaceOrUnion(astTypeCondition); - } - - public boolean isConcrete() { - return GraphQLTypeUtil.isObjectType(astTypeCondition); - } } public static class CollectNFResult { From f50013215ca71ce2b12968def11541c35d3e239d Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:53:57 +1100 Subject: [PATCH 083/393] Rename test and add better documentation --- ...NormalizedOperationFactoryDeferTest.groovy | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 95403c86d8..08ead8f5e2 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -173,24 +173,21 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "fragments on non-conditional fields - andi"() { + def "field on multiple defer declarations is associated with "() { given: - - String query = ''' query q { - dog { - ... @defer { - name - age - } - ... @defer { - age - } - } - } + dog { + ... @defer { + name + age + } + ... @defer { + age + } + } + } ''' -// This should result in age being on its own deferBlock Map variables = [:] when: @@ -206,6 +203,15 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { nameField.deferExecutions.size() == 1 ageField.deferExecutions.size() == 2 + // age field is associated with 2 defer executions, one of then is shared with "name" the other isn't + ageField.deferExecutions.any { + it == nameField.deferExecutions[0] + } + + ageField.deferExecutions.any { + it != nameField.deferExecutions[0] + } + printedTree == ['Query.dog', "Dog.name defer{[label=null;types=[Dog]]}", "Dog.age defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}", From b02efd85bae49d9fa94c62a4e43b5ccead7e2c3d Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 10 Jan 2024 09:20:50 +1100 Subject: [PATCH 084/393] Revert "WIP" This reverts commit 0025495b5f0437a5de9b2dc875da826f7ebcfb19. --- .../ExecutableNormalizedFieldTest.groovy | 3 +- ...NormalizedOperationFactoryDeferTest.groovy | 35 +------- ...tableNormalizedOperationFactoryTest.groovy | 88 +++++++++++-------- ...izedOperationToAstCompilerDeferTest.groovy | 3 +- ...ormalizedOperationToAstCompilerTest.groovy | 3 +- 5 files changed, 61 insertions(+), 71 deletions(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy index 6debbb2ff2..dc3db5daa4 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy @@ -48,7 +48,8 @@ class ExecutableNormalizedFieldTest extends Specification { """ Document document = TestUtil.parseQuery(query) - def normalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + ExecutableNormalizedOperationFactory normalizedOperationFactory = new ExecutableNormalizedOperationFactory() + def normalizedOperation = normalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def pets = normalizedOperation.getTopLevelFields()[0] def allChildren = pets.getChildren() diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 85a4fc85f6..eb2260ee11 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -143,35 +143,6 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "fragments on non-conditional fields - andi"() { - given: - - String query = ''' - query q { - dog { - ... @defer { - name - age - } - ... @defer { - age - } - } - } - ''' -// This should result in age being on its own deferBlock - Map variables = [:] - - when: - List printedTree = executeQueryAndPrintTree(query, variables) - - then: - printedTree == ['Query.dog', - "Dog.name defer{[label=null;types=[Dog]]}", - "Dog.age defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}", - ] - } - def "fragments on subset of non-conditional fields"() { given: @@ -868,8 +839,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private ExecutableNormalizedOperation createExecutableNormalizedOperations(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, null, @@ -881,8 +853,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private List executeQueryAndPrintTree(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, null, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 9a45a413db..432075a363 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -200,6 +200,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -279,6 +280,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -329,6 +331,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -371,6 +374,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -420,6 +424,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -482,6 +487,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -527,7 +533,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -571,7 +577,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -615,7 +621,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -647,7 +653,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -698,7 +704,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -748,7 +754,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -787,7 +793,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -831,6 +837,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -870,6 +877,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -917,6 +925,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) + def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1019,7 +1028,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def subFooField = (document.getDefinitions()[1] as FragmentDefinition).getSelectionSet().getSelections()[0] as Field - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1062,7 +1071,7 @@ type Dog implements Animal{ def petsField = (document.getDefinitions()[0] as OperationDefinition).getSelectionSet().getSelections()[0] as Field def idField = petsField.getSelectionSet().getSelections()[0] as Field - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1111,7 +1120,7 @@ type Dog implements Animal{ def schemaField = selections[2] as Field def typeField = selections[3] as Field - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1168,7 +1177,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1211,7 +1220,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -1239,7 +1248,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def normalizedFieldToMergedField = tree.getNormalizedFieldToMergedField() Traverser traverser = Traverser.depthFirst({ it.getChildren() }) @@ -1277,7 +1286,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) @@ -1378,7 +1387,7 @@ schema { Document document = TestUtil.parseQuery(mutation) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1428,7 +1437,7 @@ schema { assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def variables = [ var1: [bar: 123], var2: [foo: "foo", input2: [bar: 123]] @@ -1475,6 +1484,7 @@ schema { assertValidQuery(graphQLSchema, query) def document = TestUtil.parseQuery(query) + def dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) @@ -1509,6 +1519,7 @@ schema { assertValidQuery(graphQLSchema, query) def document = TestUtil.parseQuery(query) + def dependencyGraph = new ExecutableNormalizedOperationFactory() def variables = [ varIds : null, otherVar: null, @@ -1564,7 +1575,7 @@ schema { ] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) @@ -1617,7 +1628,7 @@ schema { ] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) @@ -1672,7 +1683,7 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) @@ -1730,7 +1741,7 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1778,7 +1789,7 @@ schema { ''' assertValidQuery(graphQLSchema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1854,7 +1865,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -1918,7 +1929,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -1975,7 +1986,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2050,7 +2061,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2112,7 +2123,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2154,7 +2165,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2197,7 +2208,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2240,7 +2251,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2315,7 +2326,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2391,7 +2402,7 @@ schema { ''' assertValidQuery(schema, query) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) @@ -2453,7 +2464,7 @@ schema { def variables = ["true": Boolean.TRUE, "false": Boolean.FALSE] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) println String.join("\n", printTree(tree)) @@ -2510,7 +2521,7 @@ schema { def variables = [:] assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def printedTree = printTreeAndDirectives(tree) @@ -2575,7 +2586,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2628,6 +2639,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) + def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2674,6 +2686,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) + def dependencyGraph = new ExecutableNormalizedOperationFactory() def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2871,9 +2884,10 @@ fragment personName on Person { String operationName, CoercedVariables coercedVariableValues ) { + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) + return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) } private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( @@ -2882,10 +2896,10 @@ fragment personName on Person { String operationName, RawVariables rawVariables ) { - + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, operationName, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index dab369b974..8d7f9e91a3 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -491,8 +491,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true) - return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( schema, originalDocument, null, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 27c4c89a6d..8b35c67b09 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2145,8 +2145,9 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables), options) + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables), options) } private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { From 86ac919abfde0f31cea8c7e0e103594781775509 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 10 Jan 2024 08:56:48 +1000 Subject: [PATCH 085/393] upgrade gradle to 8.5 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b1e..3499ded5c1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From ed039927ce7abecbc33ec41d19a83aa4a364482c Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:47:26 +0900 Subject: [PATCH 086/393] Propagate GraphQLContext when completing values --- src/main/java/graphql/execution/ExecutionStrategy.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 4ed0f1e644..9fcc19b7af 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -683,6 +683,7 @@ protected CompletableFuture completeValueForObject(ExecutionCon .objectType(resolvedObjectType) .fragments(executionContext.getFragmentsByName()) .variables(executionContext.getCoercedVariables().toMap()) + .graphQLContext(executionContext.getGraphQLContext()) .build(); MergedSelectionSet subFields = fieldCollector.collectFields(collectorParameters, parameters.getField()); From 58ff3a72d23604d05fa526854e9b684e353ef4b7 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:52:47 +0900 Subject: [PATCH 087/393] Tidy up --- src/test/groovy/graphql/execution/ConditionalNodesTest.groovy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy index 5b33a316b2..3b75b05b0b 100644 --- a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy +++ b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy @@ -192,8 +192,7 @@ class ConditionalNodesTest extends Specification { then: er["data"] == ["in": "in", "out": "out"] - // TODO: this test demonstrates that the GraphQLContext is missing for Pet - // and therefore, fields are being incorrectly included + // A test for fields below the top level when: ei = ExecutionInput.newExecutionInput() .graphQLContext(contextMap) @@ -210,7 +209,6 @@ class ConditionalNodesTest extends Specification { er = graphQL.execute(ei) then: - // TODO: favouriteSnack should not appear in the data er["data"] == ["in": "in", "pet": ["name": "name"]] } From cbd9b634d34fd81e91cde2ca24c356a479e635d5 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 10 Jan 2024 13:42:55 +1100 Subject: [PATCH 088/393] Fixes on IncrementalItem hierarchy --- .../graphql/incremental/DeferredItem.java | 20 ++++--- .../graphql/incremental/IncrementalItem.java | 56 ++++++++----------- .../graphql/incremental/StreamedItem.java | 19 ++++--- 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/main/java/graphql/incremental/DeferredItem.java b/src/main/java/graphql/incremental/DeferredItem.java index fcdbb65a9f..b3b3f42e87 100644 --- a/src/main/java/graphql/incremental/DeferredItem.java +++ b/src/main/java/graphql/incremental/DeferredItem.java @@ -1,16 +1,18 @@ package graphql.incremental; import graphql.ExperimentalApi; +import graphql.GraphQLError; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; @ExperimentalApi public class DeferredItem extends IncrementalItem { private final Object data; - private DeferredItem(Object data, IncrementalItem incrementalExecutionResult) { - super(incrementalExecutionResult); + private DeferredItem(Object data, List path, List errors, Map extensions) { + super(path, errors, extensions); this.data = data; } @@ -34,23 +36,23 @@ public static DeferredItem.Builder newDeferredItem() { return new DeferredItem.Builder(); } - public static class Builder extends IncrementalItem.Builder { + public static class Builder extends IncrementalItem.Builder { private Object data = null; - private final IncrementalItem.Builder builder = IncrementalItem.newIncrementalExecutionResult(); public Builder data(Object data) { this.data = data; return this; } - public Builder from(IncrementalItem incrementalExecutionResult) { - builder.from(incrementalExecutionResult); + public Builder from(DeferredItem deferredItem) { + super.from(deferredItem); + this.data = deferredItem.data; return this; } - public IncrementalItem build() { - IncrementalItem build = builder.build(); - return new DeferredItem(data, build); + @Override + public DeferredItem build() { + return new DeferredItem(data, this.path, this.errors, this.extensions); } } } diff --git a/src/main/java/graphql/incremental/IncrementalItem.java b/src/main/java/graphql/incremental/IncrementalItem.java index 9639c168e8..b504143486 100644 --- a/src/main/java/graphql/incremental/IncrementalItem.java +++ b/src/main/java/graphql/incremental/IncrementalItem.java @@ -12,34 +12,30 @@ import static java.util.stream.Collectors.toList; @ExperimentalApi -public class IncrementalItem { +public abstract class IncrementalItem { private final List path; private final List errors; private final transient Map extensions; - private IncrementalItem(List path, List errors, Map extensions) { + protected IncrementalItem(List path, List errors, Map extensions) { this.path = path; this.errors = errors; this.extensions = extensions; } - IncrementalItem(IncrementalItem incrementalItem) { - this(incrementalItem.getPath(), incrementalItem.getErrors(), incrementalItem.getExtensions()); - } - public List getPath() { - return null; + return this.path; } public List getErrors() { - return null; + return this.errors; } public Map getExtensions() { - return null; + return this.extensions; } - public Map toSpecification() { + protected Map toSpecification() { Map result = new LinkedHashMap<>(); if (errors != null && !errors.isEmpty()) { result.put("errors", errorsToSpec(errors)); @@ -53,66 +49,60 @@ public Map toSpecification() { return result; } - private Object errorsToSpec(List errors) { + protected Object errorsToSpec(List errors) { return errors.stream().map(GraphQLError::toSpecification).collect(toList()); } - static IncrementalItem.Builder newIncrementalExecutionResult() { - return new IncrementalItem.Builder(); - } + protected static abstract class Builder { + protected List path; + protected List errors = new ArrayList<>(); + protected Map extensions; - public static class Builder { - private List path; - private List errors = new ArrayList<>(); - private Map extensions; - - public IncrementalItem.Builder from(IncrementalItem incrementalExecutionResult) { - path = incrementalExecutionResult.getPath(); - errors = new ArrayList<>(incrementalExecutionResult.getErrors()); - extensions = incrementalExecutionResult.getExtensions(); + public Builder from(T incrementalExecutionResult) { + this.path = incrementalExecutionResult.getPath(); + this.errors = new ArrayList<>(incrementalExecutionResult.getErrors()); + this.extensions = incrementalExecutionResult.getExtensions(); return this; } - public IncrementalItem.Builder path(ResultPath path) { + public Builder path(ResultPath path) { if (path != null) { this.path = path.toList(); } return this; } - public IncrementalItem.Builder path(List path) { + public Builder path(List path) { this.path = path; return this; } - public IncrementalItem.Builder errors(List errors) { + public Builder errors(List errors) { this.errors = errors; return this; } - public IncrementalItem.Builder addErrors(List errors) { + public Builder addErrors(List errors) { this.errors.addAll(errors); return this; } - public IncrementalItem.Builder addError(GraphQLError error) { + public Builder addError(GraphQLError error) { this.errors.add(error); return this; } - public IncrementalItem.Builder extensions(Map extensions) { + public Builder extensions(Map extensions) { this.extensions = extensions; return this; } - public IncrementalItem.Builder addExtension(String key, Object value) { + public Builder addExtension(String key, Object value) { this.extensions = (this.extensions == null ? new LinkedHashMap<>() : this.extensions); this.extensions.put(key, value); return this; } - public IncrementalItem build() { - return new IncrementalItem(path, errors, extensions); - } + public abstract T build(); } } diff --git a/src/main/java/graphql/incremental/StreamedItem.java b/src/main/java/graphql/incremental/StreamedItem.java index b07b259f8b..269695d55d 100644 --- a/src/main/java/graphql/incremental/StreamedItem.java +++ b/src/main/java/graphql/incremental/StreamedItem.java @@ -1,6 +1,7 @@ package graphql.incremental; import graphql.ExperimentalApi; +import graphql.GraphQLError; import java.util.LinkedHashMap; import java.util.List; @@ -10,8 +11,8 @@ public class StreamedItem extends IncrementalItem { private final List items; - private StreamedItem(List items, IncrementalItem incrementalExecutionResult) { - super(incrementalExecutionResult); + private StreamedItem(List items, List path, List errors, Map extensions) { + super(path, errors, extensions); this.items = items; } @@ -35,23 +36,23 @@ public static StreamedItem.Builder newStreamedItem() { return new StreamedItem.Builder(); } - public static class Builder extends IncrementalItem.Builder { + public static class Builder extends IncrementalItem.Builder { private List items = null; - private final IncrementalItem.Builder builder = IncrementalItem.newIncrementalExecutionResult(); public Builder items(List items) { this.items = items; return this; } - public Builder from(IncrementalItem incrementalExecutionResult) { - builder.from(incrementalExecutionResult); + public Builder from(StreamedItem streamedItem) { + super.from(streamedItem); + this.items = streamedItem.items; return this; } - public IncrementalItem build() { - IncrementalItem build = builder.build(); - return new StreamedItem(items, build); + @Override + public StreamedItem build() { + return new StreamedItem(items, this.path, this.errors, this.extensions); } } } From f3e5c846333c6097be3c0b2c2708721108d0ddef Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 10 Jan 2024 14:02:40 +1100 Subject: [PATCH 089/393] Small changes after Andi's review --- .../ExecutableNormalizedOperationFactory.java | 19 ++++++------------- .../incremental/DeferExecution.java | 12 +----------- .../incremental/IncrementalNodes.java | 8 +++----- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 7116693d29..9367c1d58d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -674,18 +674,11 @@ private List groupByCommonParentsWithDeferSupport(Collectio } private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { - return deferExecution -> { - if (deferExecution.getTargetType() == null) { - return true; - } - - if (deferExecution.getTargetType().equals(objectType.getName())) { - return true; - } - - return objectType.getInterfaces().stream() - .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); - }; + String objectTypeName = objectType.getName(); + return deferExecution -> deferExecution.getPossibleTypes() + .stream() + .map(GraphQLObjectType::getName) + .anyMatch(objectTypeName::equals); } private Set listDuplicatedLabels(Collection deferExecutions) { @@ -780,7 +773,7 @@ private DeferExecution buildDeferExecution( return null; } - return incrementalNodes.getDeferExecution( + return incrementalNodes.createDeferExecution( this.coercedVariableValues.toMap(), directives, typeCondition, diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index f4bab2634f..14213ea480 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -12,12 +12,10 @@ @Internal public class DeferExecution { private final String label; - private final String targetType; private final Set possibleTypes; - public DeferExecution(@Nullable String label, @Nullable String targetType, Set possibleTypes) { + public DeferExecution(@Nullable String label, Set possibleTypes) { this.label = label; - this.targetType = targetType; this.possibleTypes = possibleTypes; } @@ -29,14 +27,6 @@ public String getLabel() { return label; } - /** - * @return the name of the type that is the target of the defer declaration - */ - @Nullable - public String getTargetType() { - return targetType; - } - /** * TODO Javadoc */ diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 47199f62a8..025b9333a0 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -21,7 +21,7 @@ @Internal public class IncrementalNodes { - public DeferExecution getDeferExecution( + public DeferExecution createDeferExecution( Map variables, List directives, @Nullable TypeName targetType, @@ -41,15 +41,13 @@ public DeferExecution getDeferExecution( Object label = argumentValues.get("label"); - String targetTypeName = targetType == null ? null : targetType.getName(); - if (label == null) { - return new DeferExecution(null, targetTypeName, possibleTypes); + return new DeferExecution(null, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label, targetTypeName, possibleTypes); + return new DeferExecution((String) label, possibleTypes); } return null; From a91fa642d98185f706650d14a9b315ef59fdeca8 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 10 Jan 2024 14:35:14 +1100 Subject: [PATCH 090/393] Add Javadoc for DeferExecution --- .../normalized/ExecutableNormalizedField.java | 5 +- .../incremental/DeferExecution.java | 97 ++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 0dfbd9a892..3a8d36af08 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -476,7 +476,10 @@ public ExecutableNormalizedField getParent() { return parent; } - // TODO: Javadoc + /** + * @return the {@link DeferExecution}s associated with this {@link ExecutableNormalizedField}. + * @see DeferExecution + */ @ExperimentalApi public LinkedHashSet getDeferExecutions() { return deferExecutions; diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 14213ea480..0f2593c2b2 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -7,7 +7,100 @@ import java.util.Set; /** - * TODO: Javadoc + * Represents details about the defer execution that can be associated with a {@link graphql.normalized.ExecutableNormalizedField}. + * + * Taking this schema as an example: + *
+ *     type Query { animal: Animal }
+ *     interface Animal { name: String, age: Int }
+ *     type Cat implements Animal { name: String, age: Int }
+ *     type Dog implements Animal { name: String, age: Int }
+ * 
+ * + * An ENF can be associated with multiple `DeferExecution`s + * + * For example, this query: + *
+ *     query MyQuery {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *             }
+ *             ... @defer {
+ *                 name
+ *             }
+ *         }
+ *     }
+ * 
+ * + * Would result in one ENF (name) associated with 2 `DeferExecution` instances. This is relevant for the execution + * since the field would have to be included in 2 incremental payloads. (I know, there's some duplication here, but + * this is the current state of the spec. There are some discussions happening around de-duplicating data in scenarios + * like this, so this behaviour might change in the future). + * + * A `DeferExecution` may be associated with a list of possible types + * + * For example, this query: + *
+ *     query MyQuery {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *             }
+ *         }
+ *     }
+ * 
+ * results in a `DeferExecution` with no label and possible types [Dog, Cat] + * + * A `DeferExecution` may be associated with specific types + * For example, this query: + *
+ *     query MyQuery {
+ *         animal {
+ *             ... on Cat @defer {
+ *                 name
+ *             }
+ *             ... on Dog {
+ *                 name
+ *             }
+ *         }
+ *     }
+ * 
+ * results in a single ENF (name) associated with a `DeferExecution` with only "Cat" as a possible type. This means + * that, at execution time, `name` should be deferred only if the return object is a "Cat" (but not a if it is a "Dog"). + * + * ENFs associated with the same instance of `DeferExecution` will be resolved in the same incremental response payload + * For example, take these queries: + * + *
+ *     query Query1 {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *             }
+ *             ... @defer {
+ *                 age
+ *             }
+ *         }
+ *     }
+ *
+ *     query Query2 {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *                 age
+ *             }
+ *         }
+ *     }
+ * 
+ * + * In `Query1`, the ENFs name and age are associated with different instances of `DeferExecution`. This means that, + * during execution, `name` and `age` can be delivered at different times (if name is resolved faster, it will be + * delivered first, and vice-versa). + * In `Query2` the fields will share the same instance of `DeferExecution`. This ensures that, at execution time, the + * fields are guaranteed to be delivered together. In other words, execution should wait until the slowest field resolves + * and deliver both fields at the same time. + * */ @Internal public class DeferExecution { @@ -28,7 +121,7 @@ public String getLabel() { } /** - * TODO Javadoc + * @return the concrete object types that are associated with this defer execution */ public Set getPossibleTypes() { return possibleTypes; From b22fe7a4bfffc7b499c30de100ec814c3fdd6294 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 11 Jan 2024 08:25:28 +1000 Subject: [PATCH 091/393] minor changes --- src/main/java/graphql/Directives.java | 8 +++++++- .../graphql/normalized/incremental/DeferExecution.java | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index fe207f47c8..8e0c81661e 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -81,10 +81,16 @@ public class Directives { /** * The @defer directive can be used to defer sending data for a fragment until later in the query. * This is an opt-in directive that is not available unless it is explicitly put into the schema. + *

+ * This implementation is based on the state of Defer/Stream PR + * More specifically at the state of this + * commit + *

+ * The execution behaviour should match what we get from running Apollo Server 4.9.5 with graphql-js v17.0.0-alpha.2 */ @ExperimentalApi public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() - .name("defer") + .name(DEFER) .description("This directive allows results to be deferred during execution") .validLocations(FRAGMENT_SPREAD, INLINE_FRAGMENT) .argument(newArgument() diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 0f2593c2b2..71488b03ca 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -1,6 +1,6 @@ package graphql.normalized.incremental; -import graphql.Internal; +import graphql.ExperimentalApi; import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; @@ -102,7 +102,7 @@ * and deliver both fields at the same time. * */ -@Internal +@ExperimentalApi public class DeferExecution { private final String label; private final Set possibleTypes; From de781ebfeda101d3228e9ffd28c693ee56d23392 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Thu, 11 Jan 2024 09:43:22 +0900 Subject: [PATCH 092/393] Update scalar coercion example --- src/test/groovy/readme/ScalarExamples.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/groovy/readme/ScalarExamples.java b/src/test/groovy/readme/ScalarExamples.java index 5d76dda856..7ffee2fc4a 100644 --- a/src/test/groovy/readme/ScalarExamples.java +++ b/src/test/groovy/readme/ScalarExamples.java @@ -1,12 +1,17 @@ package readme; +import graphql.GraphQLContext; +import graphql.execution.CoercedVariables; import graphql.language.StringValue; +import graphql.language.Value; import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; import graphql.schema.GraphQLScalarType; +import org.jetbrains.annotations.NotNull; +import java.util.Locale; import java.util.regex.Pattern; @SuppressWarnings("unused") @@ -19,17 +24,17 @@ public static class EmailScalar { .description("A custom scalar that handles emails") .coercing(new Coercing() { @Override - public Object serialize(Object dataFetcherResult) { + public Object serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { return serializeEmail(dataFetcherResult); } @Override - public Object parseValue(Object input) { + public Object parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { return parseEmailFromVariable(input); } @Override - public Object parseLiteral(Object input) { + public Object parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { return parseEmailFromAstLiteral(input); } }) @@ -73,5 +78,4 @@ private static Object parseEmailFromAstLiteral(Object input) { } } - } From 50ca55615190e677f0984bcf7f79e2a3d717df24 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Thu, 11 Jan 2024 09:47:39 +0900 Subject: [PATCH 093/393] Remove null annotations for clarity --- src/test/groovy/readme/ScalarExamples.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/groovy/readme/ScalarExamples.java b/src/test/groovy/readme/ScalarExamples.java index 7ffee2fc4a..601d949d67 100644 --- a/src/test/groovy/readme/ScalarExamples.java +++ b/src/test/groovy/readme/ScalarExamples.java @@ -9,7 +9,6 @@ import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; import graphql.schema.GraphQLScalarType; -import org.jetbrains.annotations.NotNull; import java.util.Locale; import java.util.regex.Pattern; @@ -24,17 +23,17 @@ public static class EmailScalar { .description("A custom scalar that handles emails") .coercing(new Coercing() { @Override - public Object serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + public Object serialize(Object dataFetcherResult, GraphQLContext graphQLContext, Locale locale) { return serializeEmail(dataFetcherResult); } @Override - public Object parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + public Object parseValue(Object input, GraphQLContext graphQLContext, Locale locale) { return parseEmailFromVariable(input); } @Override - public Object parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + public Object parseLiteral(Value input, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { return parseEmailFromAstLiteral(input); } }) From ceac689b682eee7a212e97a14be78ade64a6820f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 11 Jan 2024 15:08:31 +1100 Subject: [PATCH 094/393] Adjustments and plenty of Javadoc --- .../graphql/incremental/DeferPayload.java | 73 ++++++++++++++ .../graphql/incremental/DeferredItem.java | 58 ----------- .../DelayedIncrementalExecutionResult.java | 30 +++++- ...DelayedIncrementalExecutionResultImpl.java | 43 ++++---- .../IncrementalExecutionResult.java | 99 +++++++++++++++++++ .../IncrementalExecutionResultImpl.java | 34 +++++-- ...entalItem.java => IncrementalPayload.java} | 39 +++++++- .../graphql/incremental/StreamPayload.java | 73 ++++++++++++++ .../graphql/incremental/StreamedItem.java | 58 ----------- 9 files changed, 360 insertions(+), 147 deletions(-) create mode 100644 src/main/java/graphql/incremental/DeferPayload.java delete mode 100644 src/main/java/graphql/incremental/DeferredItem.java rename src/main/java/graphql/incremental/{IncrementalItem.java => IncrementalPayload.java} (73%) create mode 100644 src/main/java/graphql/incremental/StreamPayload.java delete mode 100644 src/main/java/graphql/incremental/StreamedItem.java diff --git a/src/main/java/graphql/incremental/DeferPayload.java b/src/main/java/graphql/incremental/DeferPayload.java new file mode 100644 index 0000000000..2d8d3e3594 --- /dev/null +++ b/src/main/java/graphql/incremental/DeferPayload.java @@ -0,0 +1,73 @@ +package graphql.incremental; + +import graphql.ExperimentalApi; +import graphql.GraphQLError; + +import javax.annotation.Nullable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a defer payload + */ +@ExperimentalApi +public class DeferPayload extends IncrementalPayload { + private final Object data; + + private DeferPayload(Object data, List path, String label, List errors, Map extensions) { + super(path, label, errors, extensions); + this.data = data; + } + + /** + * @return the resolved data + * @param the type to cast the result to + */ + @Nullable + public T getData() { + //noinspection unchecked + return (T) this.data; + } + + /** + * @return a map of this payload that strictly follows the spec + */ + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + + if (data != null) { + map.put("data", data); + } + + return map; + } + + /** + * @return a {@link DeferPayload.Builder} that can be used to create an instance of {@link DeferPayload} + */ + public static DeferPayload.Builder newDeferredItem() { + return new DeferPayload.Builder(); + } + + public static class Builder extends IncrementalPayload.Builder { + private Object data = null; + + public Builder data(Object data) { + this.data = data; + return this; + } + + public Builder from(DeferPayload deferredItem) { + super.from(deferredItem); + this.data = deferredItem.data; + return this; + } + + @Override + public DeferPayload build() { + return new DeferPayload(data, this.path, this.label, this.errors, this.extensions); + } + } +} diff --git a/src/main/java/graphql/incremental/DeferredItem.java b/src/main/java/graphql/incremental/DeferredItem.java deleted file mode 100644 index b3b3f42e87..0000000000 --- a/src/main/java/graphql/incremental/DeferredItem.java +++ /dev/null @@ -1,58 +0,0 @@ -package graphql.incremental; - -import graphql.ExperimentalApi; -import graphql.GraphQLError; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@ExperimentalApi -public class DeferredItem extends IncrementalItem { - private final Object data; - - private DeferredItem(Object data, List path, List errors, Map extensions) { - super(path, errors, extensions); - this.data = data; - } - - public T getData() { - //noinspection unchecked - return (T) this.data; - } - - @Override - public Map toSpecification() { - Map map = new LinkedHashMap<>(super.toSpecification()); - - if (data != null) { - map.put("data", data); - } - - return map; - } - - public static DeferredItem.Builder newDeferredItem() { - return new DeferredItem.Builder(); - } - - public static class Builder extends IncrementalItem.Builder { - private Object data = null; - - public Builder data(Object data) { - this.data = data; - return this; - } - - public Builder from(DeferredItem deferredItem) { - super.from(deferredItem); - this.data = deferredItem.data; - return this; - } - - @Override - public DeferredItem build() { - return new DeferredItem(data, this.path, this.errors, this.extensions); - } - } -} diff --git a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java index 03650be1b2..3ba520dab7 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java @@ -2,13 +2,37 @@ import graphql.ExperimentalApi; +import javax.annotation.Nullable; import java.util.List; +import java.util.Map; +/** + * Represents a result that is delivered asynchronously, after the initial {@link IncrementalExecutionResult}. + *

+ * Multiple defer and/or stream payloads (represented by {@link IncrementalPayload}) can be part of the same + * {@link DelayedIncrementalExecutionResult} + */ @ExperimentalApi public interface DelayedIncrementalExecutionResult { - List getIncremental(); - - String getLabel(); + /** + * @return a list of defer and/or stream payloads. + */ + @Nullable + List getIncremental(); + /** + * Indicates whether the stream will continue emitting {@link DelayedIncrementalExecutionResult}s after this one. + *

+ * The value returned by this method should be "true" for all but the last response in the stream. The value of this + * entry is `false` for the last response of the stream. + * + * @return "true" if there are more responses in the stream, "false" otherwise. + */ boolean hasNext(); + + /** + * @return a map of extensions or null if there are none + */ + @Nullable + Map getExtensions(); } diff --git a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java index 54645b24f0..5434db8798 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java @@ -1,60 +1,67 @@ package graphql.incremental; +import graphql.ExperimentalApi; + import java.util.Collections; import java.util.List; +import java.util.Map; +@ExperimentalApi public class DelayedIncrementalExecutionResultImpl implements DelayedIncrementalExecutionResult { - private final List incrementalItems; - private final String label; + private final List incrementalItems; private final boolean hasNext; + private final Map extensions; - private DelayedIncrementalExecutionResultImpl(List incrementalItems, String label, boolean hasNext) { - this.incrementalItems = incrementalItems; - this.label = label; - this.hasNext = hasNext; + private DelayedIncrementalExecutionResultImpl(Builder builder) { + this.incrementalItems = builder.incrementalItems; + this.hasNext = builder.hasNext; + this.extensions = builder.extensions; } @Override - public List getIncremental() { + public List getIncremental() { return this.incrementalItems; } @Override - public String getLabel() { - return this.label; + public boolean hasNext() { + return this.hasNext; } @Override - public boolean hasNext() { - return this.hasNext; + public Map getExtensions() { + return this.extensions; } + /** + * @return a {@link Builder} that can be used to create an instance of {@link DelayedIncrementalExecutionResultImpl} + */ public static Builder newIncrementalExecutionResult() { return new Builder(); } public static class Builder { private boolean hasNext = false; - private List incrementalItems = Collections.emptyList(); - private String label = null; + private List incrementalItems = Collections.emptyList(); + private Map extensions; - public DelayedIncrementalExecutionResultImpl.Builder hasNext(boolean hasNext) { + public Builder hasNext(boolean hasNext) { this.hasNext = hasNext; return this; } - public DelayedIncrementalExecutionResultImpl.Builder incrementalItems(List incrementalItems) { + public Builder incrementalItems(List incrementalItems) { this.incrementalItems = incrementalItems; return this; } - public DelayedIncrementalExecutionResultImpl.Builder label(String label) { - this.label = label; + public Builder extensions(boolean hasNext) { + this.hasNext = hasNext; return this; } public DelayedIncrementalExecutionResultImpl build() { - return new DelayedIncrementalExecutionResultImpl(this.incrementalItems, this.label, this.hasNext); + return new DelayedIncrementalExecutionResultImpl(this); } } } diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResult.java b/src/main/java/graphql/incremental/IncrementalExecutionResult.java index d0178092b7..32fc2539ba 100644 --- a/src/main/java/graphql/incremental/IncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/IncrementalExecutionResult.java @@ -4,9 +4,108 @@ import graphql.ExperimentalApi; import org.reactivestreams.Publisher; +import javax.annotation.Nullable; +import java.util.List; + +/** + * A result that is part of an execution that includes incrementally delivered data (data has been deferred of streamed). + *

+ * For example, this query + *

+ * query {
+ *   person(id: "cGVvcGxlOjE=") {
+ *     ...HomeWorldFragment @defer(label: "homeWorldDefer")
+ *     name
+ *     films @stream(initialCount: 1, label: "filmsStream") {
+ *       title
+ *     }
+ *   }
+ * }
+ * fragment HomeWorldFragment on Person {
+ *   homeWorld {
+ *     name
+ *   }
+ * }
+ * 
+ * Could result on an incremental response with the following payloads (in JSON format here for simplicity). + *

+ * Response 1, the initial response does not contain any deferred or streamed results. + *

+ * {
+ *   "data": {
+ *     "person": {
+ *       "name": "Luke Skywalker",
+ *       "films": [{ "title": "A New Hope" }]
+ *     }
+ *   },
+ *   "hasNext": true
+ * }
+ * 
+ * + * Response 2, contains the defer payload and the first stream payload. + *
+ * {
+ *   "incremental": [
+ *     {
+ *       "label": "homeWorldDefer",
+ *       "path": ["person"],
+ *       "data": { "homeWorld": { "name": "Tatooine" } }
+ *     },
+ *     {
+ *       "label": "filmsStream",
+ *       "path": ["person", "films", 1],
+ *       "items": [{ "title": "The Empire Strikes Back" }]
+ *     }
+ *   ],
+ *   "hasNext": true
+ * }
+ * 
+ * + * Response 3, contains the final stream payload. Note how "hasNext" is "false", indicating this is the final response. + *
+ * {
+ *   "incremental": [
+ *     {
+ *       "label": "filmsStream",
+ *       "path": ["person", "films", 2],
+ *       "items": [{ "title": "Return of the Jedi" }]
+ *     }
+ *   ],
+ *   "hasNext": false
+ * }
+ * 
+ * + *

+ * This implementation is based on the state of Defer/Stream PR + * More specifically at the state of this + * commit + *

+ * The execution behaviour should match what we get from running Apollo Server 4.9.5 with graphql-js v17.0.0-alpha.2 + */ @ExperimentalApi public interface IncrementalExecutionResult extends ExecutionResult { + /** + * Indicates whether there are pending incremental data. + * @return "true" if there are incremental data, "false" otherwise. + */ boolean hasNext(); + /** + * Returns a list of defer and/or stream payloads that the execution engine decided (for whatever reason) to resolve at the same time as the initial payload. + *

+ * (...)this field may appear on both the initial and subsequent values. + *

+ * source + * + * @return a list of Stream and/or Defer payloads that were resolved at the same time as the initial payload. + */ + @Nullable + List getIncremental(); + + /** + * This {@link Publisher} will asynchronously emit events containing defer and/or stream payloads. + * + * @return a {@link Publisher} that clients can subscribe to receive incremental payloads. + */ Publisher getIncrementalItemPublisher(); } diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java index c12b4d40d4..9478e72e7a 100644 --- a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java @@ -1,20 +1,24 @@ package graphql.incremental; import graphql.ExecutionResultImpl; +import graphql.ExperimentalApi; import org.reactivestreams.Publisher; +import javax.annotation.Nullable; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +@ExperimentalApi public class IncrementalExecutionResultImpl extends ExecutionResultImpl implements IncrementalExecutionResult { private final boolean hasNext; + private final List incremental; private final Publisher incrementalItemPublisher; - private IncrementalExecutionResultImpl( - Builder builder - ) { + private IncrementalExecutionResultImpl(Builder builder) { super(builder); this.hasNext = builder.hasNext; + this.incremental = builder.incremental; this.incrementalItemPublisher = builder.incrementalItemPublisher; } @@ -23,11 +27,20 @@ public boolean hasNext() { return this.hasNext; } + @Nullable + @Override + public List getIncremental() { + return this.incremental; + } + @Override public Publisher getIncrementalItemPublisher() { return incrementalItemPublisher; } + /** + * @return a {@link Builder} that can be used to create an instance of {@link IncrementalExecutionResultImpl} + */ public static Builder newIncrementalExecutionResult() { return new Builder(); } @@ -41,6 +54,7 @@ public Map toSpecification() { public static class Builder extends ExecutionResultImpl.Builder { private boolean hasNext = true; + public List incremental; private Publisher incrementalItemPublisher; public Builder hasNext(boolean hasNext) { @@ -48,15 +62,21 @@ public Builder hasNext(boolean hasNext) { return this; } + public Builder incremental(List incremental) { + this.incremental = incremental; + return this; + } + public Builder incrementalItemPublisher(Publisher incrementalItemPublisher) { this.incrementalItemPublisher = incrementalItemPublisher; return this; } -// public Builder from(ExecutionResult executionResult) { -// builder.from(executionResult); -// return this; -// } + public Builder from(IncrementalExecutionResult incrementalExecutionResult) { + super.from(incrementalExecutionResult); + this.hasNext = incrementalExecutionResult.hasNext(); + return this; + } public IncrementalExecutionResult build() { return new IncrementalExecutionResultImpl(this); diff --git a/src/main/java/graphql/incremental/IncrementalItem.java b/src/main/java/graphql/incremental/IncrementalPayload.java similarity index 73% rename from src/main/java/graphql/incremental/IncrementalItem.java rename to src/main/java/graphql/incremental/IncrementalPayload.java index b504143486..74c9980912 100644 --- a/src/main/java/graphql/incremental/IncrementalItem.java +++ b/src/main/java/graphql/incremental/IncrementalPayload.java @@ -4,6 +4,8 @@ import graphql.GraphQLError; import graphql.execution.ResultPath; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -11,26 +13,50 @@ import static java.util.stream.Collectors.toList; +/** + * Represents a payload that can be resolved after the initial response. + */ @ExperimentalApi -public abstract class IncrementalItem { +public abstract class IncrementalPayload { private final List path; + private final String label; private final List errors; private final transient Map extensions; - protected IncrementalItem(List path, List errors, Map extensions) { + protected IncrementalPayload(List path, String label, List errors, Map extensions) { this.path = path; this.errors = errors; + this.label = label; this.extensions = extensions; } + /** + * @return list of field names and indices from root to the location of the corresponding `@defer` or `@stream` directive. + */ public List getPath() { return this.path; } + /** + * @return value derived from the corresponding `@defer` or `@stream` directive. + */ + @Nullable + public String getLabel() { + return label; + } + + /** + * @return a list of field errors encountered during execution. + */ + @Nullable public List getErrors() { return this.errors; } + /** + * @return a map of extensions or null if there are none + */ + @Nullable public Map getExtensions() { return this.extensions; } @@ -53,13 +79,15 @@ protected Object errorsToSpec(List errors) { return errors.stream().map(GraphQLError::toSpecification).collect(toList()); } - protected static abstract class Builder { + protected static abstract class Builder { protected List path; + protected String label; protected List errors = new ArrayList<>(); protected Map extensions; public Builder from(T incrementalExecutionResult) { this.path = incrementalExecutionResult.getPath(); + this.label = incrementalExecutionResult.getLabel(); this.errors = new ArrayList<>(incrementalExecutionResult.getErrors()); this.extensions = incrementalExecutionResult.getExtensions(); return this; @@ -77,6 +105,11 @@ public Builder path(List path) { return this; } + public Builder label(String label) { + this.label = label; + return this; + } + public Builder errors(List errors) { this.errors = errors; return this; diff --git a/src/main/java/graphql/incremental/StreamPayload.java b/src/main/java/graphql/incremental/StreamPayload.java new file mode 100644 index 0000000000..adb1581ac9 --- /dev/null +++ b/src/main/java/graphql/incremental/StreamPayload.java @@ -0,0 +1,73 @@ +package graphql.incremental; + +import graphql.ExperimentalApi; +import graphql.GraphQLError; + +import javax.annotation.Nullable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a stream payload + */ +@ExperimentalApi +public class StreamPayload extends IncrementalPayload { + private final List items; + + private StreamPayload(List items, List path, String label, List errors, Map extensions) { + super(path, label, errors, extensions); + this.items = items; + } + + /** + * @return the resolved list of items + * @param the type to cast the result to + */ + @Nullable + public List getItems() { + //noinspection unchecked + return (List) this.items; + } + + /** + * @return a map of this payload that strictly follows the spec + */ + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + + if (items != null) { + map.put("items", items); + } + + return map; + } + + /** + * @return a {@link Builder} that can be used to create an instance of {@link StreamPayload} + */ + public static StreamPayload.Builder newStreamedItem() { + return new StreamPayload.Builder(); + } + + public static class Builder extends IncrementalPayload.Builder { + private List items = null; + + public Builder items(List items) { + this.items = items; + return this; + } + + public Builder from(StreamPayload streamedItem) { + super.from(streamedItem); + this.items = streamedItem.items; + return this; + } + + @Override + public StreamPayload build() { + return new StreamPayload(items, this.path, this.label, this.errors, this.extensions); + } + } +} diff --git a/src/main/java/graphql/incremental/StreamedItem.java b/src/main/java/graphql/incremental/StreamedItem.java deleted file mode 100644 index 269695d55d..0000000000 --- a/src/main/java/graphql/incremental/StreamedItem.java +++ /dev/null @@ -1,58 +0,0 @@ -package graphql.incremental; - -import graphql.ExperimentalApi; -import graphql.GraphQLError; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@ExperimentalApi -public class StreamedItem extends IncrementalItem { - private final List items; - - private StreamedItem(List items, List path, List errors, Map extensions) { - super(path, errors, extensions); - this.items = items; - } - - public List getItems() { - //noinspection unchecked - return (List) this.items; - } - - @Override - public Map toSpecification() { - Map map = new LinkedHashMap<>(super.toSpecification()); - - if (items != null) { - map.put("items", items); - } - - return map; - } - - public static StreamedItem.Builder newStreamedItem() { - return new StreamedItem.Builder(); - } - - public static class Builder extends IncrementalItem.Builder { - private List items = null; - - public Builder items(List items) { - this.items = items; - return this; - } - - public Builder from(StreamedItem streamedItem) { - super.from(streamedItem); - this.items = streamedItem.items; - return this; - } - - @Override - public StreamedItem build() { - return new StreamedItem(items, this.path, this.errors, this.extensions); - } - } -} From a8f790641026eb5be249c8b72e14b3e14a935a50 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 11 Jan 2024 17:41:19 +1100 Subject: [PATCH 095/393] Add unit test for sanity check --- .../graphql/incremental/DeferPayload.java | 3 +- .../IncrementalExecutionResultImpl.java | 13 ++++ .../incremental/IncrementalPayload.java | 45 +++++++------- .../graphql/incremental/StreamPayload.java | 3 +- .../IncrementalExecutionResultTest.groovy | 59 +++++++++++++++++++ 5 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy diff --git a/src/main/java/graphql/incremental/DeferPayload.java b/src/main/java/graphql/incremental/DeferPayload.java index 2d8d3e3594..2327493ef5 100644 --- a/src/main/java/graphql/incremental/DeferPayload.java +++ b/src/main/java/graphql/incremental/DeferPayload.java @@ -51,7 +51,7 @@ public static DeferPayload.Builder newDeferredItem() { return new DeferPayload.Builder(); } - public static class Builder extends IncrementalPayload.Builder { + public static class Builder extends IncrementalPayload.Builder { private Object data = null; public Builder data(Object data) { @@ -65,7 +65,6 @@ public Builder from(DeferPayload deferredItem) { return this; } - @Override public DeferPayload build() { return new DeferPayload(data, this.path, this.label, this.errors, this.extensions); } diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java index 9478e72e7a..5b16cdaf50 100644 --- a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java @@ -6,8 +6,12 @@ import javax.annotation.Nullable; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; @ExperimentalApi public class IncrementalExecutionResultImpl extends ExecutionResultImpl implements IncrementalExecutionResult { @@ -49,6 +53,15 @@ public static Builder newIncrementalExecutionResult() { public Map toSpecification() { Map map = new LinkedHashMap<>(super.toSpecification()); map.put("hasNext", hasNext); + + if (this.incremental != null) { + map.put("incremental", + this.incremental.stream() + .map(IncrementalPayload::toSpecification) + .collect(Collectors.toCollection(LinkedList::new)) + ); + } + return map; } diff --git a/src/main/java/graphql/incremental/IncrementalPayload.java b/src/main/java/graphql/incremental/IncrementalPayload.java index 74c9980912..a3ddd55826 100644 --- a/src/main/java/graphql/incremental/IncrementalPayload.java +++ b/src/main/java/graphql/incremental/IncrementalPayload.java @@ -4,7 +4,6 @@ import graphql.GraphQLError; import graphql.execution.ResultPath; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -63,15 +62,19 @@ public Map getExtensions() { protected Map toSpecification() { Map result = new LinkedHashMap<>(); + + result.put("path", path); + + if (label != null) { + result.put("label", label); + } + if (errors != null && !errors.isEmpty()) { result.put("errors", errorsToSpec(errors)); } if (extensions != null) { result.put("extensions", extensions); } - if (path != null) { - result.put("path", path); - } return result; } @@ -79,40 +82,42 @@ protected Object errorsToSpec(List errors) { return errors.stream().map(GraphQLError::toSpecification).collect(toList()); } - protected static abstract class Builder { + protected static abstract class Builder> { protected List path; protected String label; protected List errors = new ArrayList<>(); protected Map extensions; - public Builder from(T incrementalExecutionResult) { - this.path = incrementalExecutionResult.getPath(); - this.label = incrementalExecutionResult.getLabel(); - this.errors = new ArrayList<>(incrementalExecutionResult.getErrors()); - this.extensions = incrementalExecutionResult.getExtensions(); - return this; + public T from(IncrementalPayload incrementalPayload) { + this.path = incrementalPayload.getPath(); + this.label = incrementalPayload.getLabel(); + if (incrementalPayload.getErrors() != null) { + this.errors = new ArrayList<>(incrementalPayload.getErrors()); + } + this.extensions = incrementalPayload.getExtensions(); + return (T) this; } - public Builder path(ResultPath path) { + public T path(ResultPath path) { if (path != null) { this.path = path.toList(); } - return this; + return (T) this; } - public Builder path(List path) { + public T path(List path) { this.path = path; - return this; + return (T) this; } - public Builder label(String label) { + public T label(String label) { this.label = label; - return this; + return (T) this; } - public Builder errors(List errors) { + public T errors(List errors) { this.errors = errors; - return this; + return (T) this; } public Builder addErrors(List errors) { @@ -135,7 +140,5 @@ public Builder addExtension(String key, Object value) { this.extensions.put(key, value); return this; } - - public abstract T build(); } } diff --git a/src/main/java/graphql/incremental/StreamPayload.java b/src/main/java/graphql/incremental/StreamPayload.java index adb1581ac9..e8bdfcf85c 100644 --- a/src/main/java/graphql/incremental/StreamPayload.java +++ b/src/main/java/graphql/incremental/StreamPayload.java @@ -51,7 +51,7 @@ public static StreamPayload.Builder newStreamedItem() { return new StreamPayload.Builder(); } - public static class Builder extends IncrementalPayload.Builder { + public static class Builder extends IncrementalPayload.Builder { private List items = null; public Builder items(List items) { @@ -65,7 +65,6 @@ public Builder from(StreamPayload streamedItem) { return this; } - @Override public StreamPayload build() { return new StreamPayload(items, this.path, this.label, this.errors, this.extensions); } diff --git a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy new file mode 100644 index 0000000000..02e8d03898 --- /dev/null +++ b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy @@ -0,0 +1,59 @@ +package graphql.incremental + +import graphql.execution.ResultPath +import spock.lang.Specification + +import static graphql.incremental.DeferPayload.newDeferredItem +import static graphql.incremental.StreamPayload.newStreamedItem + +class IncrementalExecutionResultTest extends Specification { + + def "sanity test to check builders work"() { + when: + def defer1 = newDeferredItem() + .label("homeWorldDefer") + .path(ResultPath.parse("/person")) + .data([homeWorld: "Tatooine"]) + .build() + + def stream1 = newStreamedItem() + .label("filmsStream") + .path(ResultPath.parse("/person/films[1]")) + .items([[title: "The Empire Strikes Back"]]) + .build() + + def stream2 = newStreamedItem() + .label("filmsStream") + .path(ResultPath.parse("/person/films[2]")) + .items([[title: "Return of the Jedi"]]) + .build() + + def result = IncrementalExecutionResultImpl + .newIncrementalExecutionResult() + .data([ + person: [ + name : "Luke Skywalker", + films: [ + [title: "A New Hope"] + ] + ] + ]) + .hasNext(true) + .incremental([defer1, stream1, stream2]) + .build() + + def toSpec = result.toSpecification() + + then: + toSpec == [ + data : [person: [name: "Luke Skywalker", films: [[title: "A New Hope"]]]], + hasNext : true, + incremental: [ + [path: ["person"], label: "homeWorldDefer", data: [homeWorld: "Tatooine"]], + [path: ["person", "films", 1], label: "filmsStream", items: [[title: "The Empire Strikes Back"]]], + [path: ["person", "films", 2], label: "filmsStream", items: [[title: "Return of the Jedi"]]], + ] + ] + + } +} From 6b52b0247c6167b7461a3652b1e32a2098f2d114 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 11 Jan 2024 17:46:12 +1100 Subject: [PATCH 096/393] Fix Javadoc --- .../java/graphql/incremental/IncrementalExecutionResult.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResult.java b/src/main/java/graphql/incremental/IncrementalExecutionResult.java index 32fc2539ba..6794f01185 100644 --- a/src/main/java/graphql/incremental/IncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/IncrementalExecutionResult.java @@ -61,7 +61,7 @@ * } * * - * Response 3, contains the final stream payload. Note how "hasNext" is "false", indicating this is the final response. + * Response 3, contains the final stream payload. Note how "hasNext" is "false", indicating this is the final response. *
  * {
  *   "incremental": [
@@ -93,7 +93,7 @@ public interface IncrementalExecutionResult extends ExecutionResult {
     /**
      * Returns a list of defer and/or stream payloads that the execution engine decided (for whatever reason) to resolve at the same time as the initial payload.
      * 

- * (...)this field may appear on both the initial and subsequent values. + * (...)this field may appear on both the initial and subsequent values. *

* source * From d13b50999e54130361b4d0499815c33196321218 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 12 Jan 2024 08:57:19 +1100 Subject: [PATCH 097/393] Minor adjustment in test class --- .../graphql/incremental/IncrementalExecutionResultTest.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy index 02e8d03898..d30747fccc 100644 --- a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy +++ b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy @@ -4,6 +4,7 @@ import graphql.execution.ResultPath import spock.lang.Specification import static graphql.incremental.DeferPayload.newDeferredItem +import static graphql.incremental.IncrementalExecutionResultImpl.newIncrementalExecutionResult import static graphql.incremental.StreamPayload.newStreamedItem class IncrementalExecutionResultTest extends Specification { @@ -28,8 +29,7 @@ class IncrementalExecutionResultTest extends Specification { .items([[title: "Return of the Jedi"]]) .build() - def result = IncrementalExecutionResultImpl - .newIncrementalExecutionResult() + def result = newIncrementalExecutionResult() .data([ person: [ name : "Luke Skywalker", From 291cfbf1be0e5364e2fdf59306fc54756523a204 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 12 Jan 2024 14:31:42 +1100 Subject: [PATCH 098/393] Reverts both merge commits that deleted the old defer stuff. Revert "Merge pull request #1964 from graphql-java/remove-deferred-2" This reverts commit a0d327d7821e92b173acb6d49297935343540994, reversing changes made to 3101f4838b6918ee0674d21cee0bae026411b1f0. Revert "Merge pull request #1961 from graphql-java/remove-deferred-support" This reverts commit 3101f4838b6918ee0674d21cee0bae026411b1f0, reversing changes made to 10eeacc3979624964ef10ea1078ef84534d9c5cc. --- .../java/graphql/DeferredExecutionResult.java | 16 + .../graphql/DeferredExecutionResultImpl.java | 68 ++++ src/main/java/graphql/GraphQL.java | 6 + .../execution/AsyncExecutionStrategy.java | 75 +++- .../java/graphql/execution/Execution.java | 25 +- .../graphql/execution/ExecutionContext.java | 8 + .../graphql/execution/ExecutionStrategy.java | 8 + .../ExecutionStrategyParameters.java | 18 +- .../graphql/execution/defer/DeferSupport.java | 82 +++++ .../graphql/execution/defer/DeferredCall.java | 43 +++ .../execution/defer/DeferredErrorSupport.java | 31 ++ .../ChainedInstrumentation.java | 34 ++ .../DeferredFieldInstrumentationContext.java | 12 + .../instrumentation/Instrumentation.java | 9 + .../DataLoaderDispatcherInstrumentation.java | 22 ++ .../FieldLevelTrackingApproach.java | 34 ++ ...nstrumentationDeferredFieldParameters.java | 41 +++ .../java/graphql/validation/Validator.java | 16 + .../rules/DeferredDirectiveAbstractRule.java | 38 ++ .../DeferredDirectiveOnNonNullableField.java | 32 ++ .../DeferredDirectiveOnQueryOperation.java | 41 +++ .../rules/DeferredMustBeOnAllFields.java | 100 ++++++ .../groovy/example/http/DeferHttpSupport.java | 115 ++++++ src/test/groovy/example/http/HttpMain.java | 4 + .../execution/defer/BasicSubscriber.groovy | 35 ++ .../defer/CapturingSubscriber.groovy | 45 +++ .../defer/DeferSupportIntegrationTest.groovy | 328 ++++++++++++++++++ .../execution/defer/DeferSupportTest.groovy | 272 +++++++++++++++ .../execution/defer/DeferredCallTest.groovy | 48 +++ .../defer/DeferredErrorSupportTest.groovy | 77 ++++ .../LegacyTestingInstrumentation.groovy | 7 + .../DataLoaderPerformanceTest.groovy | 64 +++- ...manceWithChainedInstrumentationTest.groovy | 63 +++- .../DeferredDirectiveAbstractRuleTest.groovy | 40 +++ ...rredDirectiveOnNonNullableFieldTest.groovy | 46 +++ ...ferredDirectiveOnQueryOperationTest.groovy | 71 ++++ .../DeferredMustBeOnAllFieldsTest.groovy | 193 +++++++++++ src/test/groovy/readme/DeferredExamples.java | 96 +++++ 38 files changed, 2257 insertions(+), 6 deletions(-) create mode 100644 src/main/java/graphql/DeferredExecutionResult.java create mode 100644 src/main/java/graphql/DeferredExecutionResultImpl.java create mode 100644 src/main/java/graphql/execution/defer/DeferSupport.java create mode 100644 src/main/java/graphql/execution/defer/DeferredCall.java create mode 100644 src/main/java/graphql/execution/defer/DeferredErrorSupport.java create mode 100644 src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java create mode 100644 src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java create mode 100644 src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java create mode 100644 src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java create mode 100644 src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java create mode 100644 src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java create mode 100644 src/test/groovy/example/http/DeferHttpSupport.java create mode 100644 src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy create mode 100644 src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy create mode 100644 src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy create mode 100644 src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy create mode 100644 src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy create mode 100644 src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy create mode 100644 src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy create mode 100644 src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy create mode 100644 src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy create mode 100644 src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy create mode 100644 src/test/groovy/readme/DeferredExamples.java diff --git a/src/main/java/graphql/DeferredExecutionResult.java b/src/main/java/graphql/DeferredExecutionResult.java new file mode 100644 index 0000000000..18eb23c74a --- /dev/null +++ b/src/main/java/graphql/DeferredExecutionResult.java @@ -0,0 +1,16 @@ +package graphql; + +import java.util.List; + +/** + * Results that come back from @defer fields have an extra path property that tells you where + * that deferred result came in the original query + */ +@PublicApi +public interface DeferredExecutionResult extends ExecutionResult { + + /** + * @return the execution path of this deferred result in the original query + */ + List getPath(); +} diff --git a/src/main/java/graphql/DeferredExecutionResultImpl.java b/src/main/java/graphql/DeferredExecutionResultImpl.java new file mode 100644 index 0000000000..c47a9a5312 --- /dev/null +++ b/src/main/java/graphql/DeferredExecutionResultImpl.java @@ -0,0 +1,68 @@ +package graphql; + +import graphql.execution.ExecutionPath; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static graphql.Assert.assertNotNull; + +/** + * Results that come back from @defer fields have an extra path property that tells you where + * that deferred result came in the original query + */ +@PublicApi +public class DeferredExecutionResultImpl extends ExecutionResultImpl implements DeferredExecutionResult { + + private final List path; + + private DeferredExecutionResultImpl(List path, ExecutionResultImpl executionResult) { + super(executionResult); + this.path = assertNotNull(path); + } + + /** + * @return the execution path of this deferred result in the original query + */ + public List getPath() { + return path; + } + + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + map.put("path", path); + return map; + } + + public static Builder newDeferredExecutionResult() { + return new Builder(); + } + + public static class Builder { + private List path = Collections.emptyList(); + private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); + + public Builder path(ExecutionPath path) { + this.path = assertNotNull(path).toList(); + return this; + } + + public Builder from(ExecutionResult executionResult) { + builder.from((ExecutionResultImpl) executionResult); + return this; + } + + public Builder addErrors(List errors) { + builder.addErrors(errors); + return this; + } + + public DeferredExecutionResult build() { + ExecutionResultImpl build = builder.build(); + return new DeferredExecutionResultImpl(path, build); + } + } +} diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 2ccb54b955..b49902fda2 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -91,6 +91,12 @@ @PublicApi public class GraphQL { + /** + * When @defer directives are used, this is the extension key name used to contain the {@link org.reactivestreams.Publisher} + * of deferred results + */ + public static final String DEFERRED_RESULTS = "deferredResults"; + private static final Logger log = LoggerFactory.getLogger(GraphQL.class); private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(GraphQL.class); diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 6608fba0f3..8689fb54cf 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -1,15 +1,28 @@ package graphql.execution; import graphql.ExecutionResult; +import graphql.execution.defer.DeferSupport; +import graphql.execution.defer.DeferredCall; +import graphql.execution.defer.DeferredErrorSupport; +import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.PublicApi; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLObjectType; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import static graphql.execution.MergedSelectionSet.newMergedSelectionSet; /** * The standard graphql execution strategy that runs fields asynchronously non-blocking. @@ -52,7 +65,14 @@ public CompletableFuture execute(ExecutionContext executionCont ExecutionStrategyParameters newParameters = parameters .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); - CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); + CompletableFuture future; + + if (isDeferred(executionContext, newParameters, currentField)) { + executionStrategyCtx.onDeferredField(currentField); + future = resolveFieldWithInfoToNull(executionContext, newParameters); + } else { + future = resolveFieldWithInfo(executionContext, newParameters); + } futures.add(future); } CompletableFuture overallResult = new CompletableFuture<>(); @@ -83,4 +103,57 @@ public CompletableFuture execute(ExecutionContext executionCont overallResult.whenComplete(executionStrategyCtx::onCompleted); return overallResult; } + + private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedField currentField) { + DeferSupport deferSupport = executionContext.getDeferSupport(); + if (deferSupport.checkForDeferDirective(currentField, executionContext.getVariables())) { + DeferredErrorSupport errorSupport = new DeferredErrorSupport(); + + // with a deferred field we are really resetting where we execute from, that is from this current field onwards + Map fields = new LinkedHashMap<>(); + fields.put(currentField.getName(), currentField); + + ExecutionStrategyParameters callParameters = parameters.transform(builder -> + { + MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); + builder.deferredErrorSupport(errorSupport) + .field(currentField) + .fields(mergedSelectionSet) + .parent(null) // this is a break in the parent -> child chain - its a new start effectively + .listSize(0) + .currentListIndex(0); + } + ); + + DeferredCall call = new DeferredCall(parameters.getPath(), deferredExecutionResult(executionContext, callParameters), errorSupport); + deferSupport.enqueue(call); + return true; + } + return false; + } + + @SuppressWarnings("FutureReturnValueIgnored") + private Supplier> deferredExecutionResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + return () -> { + GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); + GraphQLObjectType fieldContainer = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); + + Instrumentation instrumentation = executionContext.getInstrumentation(); + DeferredFieldInstrumentationContext fieldCtx = instrumentation.beginDeferredField( + new InstrumentationDeferredFieldParameters(executionContext, parameters, fieldDef, createExecutionStepInfo(executionContext, parameters, fieldDef, fieldContainer)) + ); + CompletableFuture result = new CompletableFuture<>(); + fieldCtx.onDispatched(result); + CompletableFuture fieldValueInfoFuture = resolveFieldWithInfo(executionContext, parameters); + + fieldValueInfoFuture.whenComplete((fieldValueInfo, throwable) -> { + fieldCtx.onFieldValueInfo(fieldValueInfo); + + CompletableFuture execResultFuture = fieldValueInfo.getFieldValue(); + execResultFuture = execResultFuture.whenComplete(fieldCtx::onCompleted); + Async.copyResults(execResultFuture, result); + }); + return result; + }; + } } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 916bc64659..304a8e2e3b 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -1,12 +1,15 @@ package graphql.execution; +import graphql.DeferredExecutionResult; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; +import graphql.GraphQL; import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; +import graphql.execution.defer.DeferSupport; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -22,6 +25,7 @@ import graphql.schema.GraphQLSchema; import graphql.schema.impl.SchemaUtil; import graphql.util.LogKit; +import org.reactivestreams.Publisher; import org.slf4j.Logger; import java.util.Collections; @@ -177,7 +181,26 @@ private CompletableFuture executeOperation(ExecutionContext exe result = result.thenApply(er -> mergeExtensionsBuilderIfPresent(er, graphQLContext)); result = result.whenComplete(executeOperationCtx::onCompleted); - return result; + + return deferSupport(executionContext, result); + } + + /* + * Adds the deferred publisher if its needed at the end of the query. This is also a good time for the deferred code to start running + */ + private CompletableFuture deferSupport(ExecutionContext executionContext, CompletableFuture result) { + return result.thenApply(er -> { + DeferSupport deferSupport = executionContext.getDeferSupport(); + if (deferSupport.isDeferDetected()) { + // we start the rest of the query now to maximize throughput. We have the initial important results + // and now we can start the rest of the calls as early as possible (even before some one subscribes) + Publisher publisher = deferSupport.startDeferredCalls(); + return ExecutionResultImpl.newExecutionResult().from(er) + .addExtension(GraphQL.DEFERRED_RESULTS, publisher) + .build(); + } + return er; + }); } private void addExtensionsBuilderNotPresent(GraphQLContext graphQLContext) { diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 747f955d1a..28c8f7f6da 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -6,8 +6,11 @@ import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.GraphQLError; +import graphql.Internal; import graphql.PublicApi; import graphql.collect.ImmutableKit; +import graphql.cachecontrol.CacheControl; +import graphql.execution.defer.DeferSupport; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; @@ -53,6 +56,7 @@ public class ExecutionContext { private final Set errorPaths = new HashSet<>(); private final DataLoaderRegistry dataLoaderRegistry; private final Locale locale; + private final DeferSupport deferSupport = new DeferSupport(); private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; @@ -255,6 +259,10 @@ public ExecutionStrategy getSubscriptionStrategy() { return subscriptionStrategy; } + public DeferSupport getDeferSupport() { + return deferSupport; + } + public ExecutionStrategy getStrategy(OperationDefinition.Operation operation) { if (operation == OperationDefinition.Operation.MUTATION) { return getMutationStrategy(); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 4ed0f1e644..3b35947c3b 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -372,6 +372,10 @@ protected CompletableFuture handleFetchingException( .exception(e) .build(); + // TODO: parameters here is an instance of ExecutionStrategyParameters + // TODO: not sure if this method call goes here, inside the try block below, or in the async method + parameters.deferredErrorSupport().onFetchingException(parameters, e); + try { return asyncHandleException(dataFetcherExceptionHandler, handlerParameters); } catch (Exception handlerException) { @@ -496,6 +500,7 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra logNotSafe.warn(error.getMessage(), e); context.addError(error); + parameters.deferredErrorSupport().onError(error); } /** @@ -708,6 +713,7 @@ private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategy logNotSafe.warn(error.getMessage(), e); context.addError(error); + parameters.deferredErrorSupport().onError(error); return null; } @@ -735,6 +741,8 @@ private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrate TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getExecutionStepInfo().getUnwrappedNonNullType()); logNotSafe.warn("{} got {}", error.getMessage(), result.getClass()); context.addError(error); + + parameters.deferredErrorSupport().onError(error); } diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index b413e4321a..42b6db6572 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -2,6 +2,7 @@ import graphql.Assert; import graphql.PublicApi; +import graphql.execution.defer.DeferredErrorSupport; import java.util.function.Consumer; @@ -20,6 +21,7 @@ public class ExecutionStrategyParameters { private final ResultPath path; private final MergedField currentField; private final ExecutionStrategyParameters parent; + private final DeferredErrorSupport deferredErrorSupport; private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, Object source, @@ -28,7 +30,8 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, NonNullableFieldValidator nonNullableFieldValidator, ResultPath path, MergedField currentField, - ExecutionStrategyParameters parent) { + ExecutionStrategyParameters parent, + DeferredErrorSupport deferredErrorSupport) { this.executionStepInfo = assertNotNull(executionStepInfo, () -> "executionStepInfo is null"); this.localContext = localContext; @@ -38,6 +41,7 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, this.path = path; this.currentField = currentField; this.parent = parent; + this.deferredErrorSupport = deferredErrorSupport; } public ExecutionStepInfo getExecutionStepInfo() { @@ -68,6 +72,10 @@ public ExecutionStrategyParameters getParent() { return parent; } + public DeferredErrorSupport deferredErrorSupport() { + return deferredErrorSupport; + } + /** * This returns the current field in its query representations. * @@ -106,6 +114,7 @@ public static class Builder { ResultPath path = ResultPath.rootPath(); MergedField currentField; ExecutionStrategyParameters parent; + DeferredErrorSupport deferredErrorSupport = new DeferredErrorSupport(); /** * @see ExecutionStrategyParameters#newParameters() @@ -123,6 +132,7 @@ private Builder(ExecutionStrategyParameters oldParameters) { this.fields = oldParameters.fields; this.nonNullableFieldValidator = oldParameters.nonNullableFieldValidator; this.currentField = oldParameters.currentField; + this.deferredErrorSupport = oldParameters.deferredErrorSupport; this.path = oldParameters.path; this.parent = oldParameters.parent; } @@ -172,9 +182,13 @@ public Builder parent(ExecutionStrategyParameters parent) { return this; } + public Builder deferredErrorSupport(DeferredErrorSupport deferredErrorSupport) { + this.deferredErrorSupport = deferredErrorSupport; + return this; + } public ExecutionStrategyParameters build() { - return new ExecutionStrategyParameters(executionStepInfo, source, localContext, fields, nonNullableFieldValidator, path, currentField, parent); + return new ExecutionStrategyParameters(executionStepInfo, source, localContext, fields, nonNullableFieldValidator, path, currentField, parent, deferredErrorSupport); } } } diff --git a/src/main/java/graphql/execution/defer/DeferSupport.java b/src/main/java/graphql/execution/defer/DeferSupport.java new file mode 100644 index 0000000000..7e6fb97429 --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferSupport.java @@ -0,0 +1,82 @@ +package graphql.execution.defer; + +import graphql.DeferredExecutionResult; +import graphql.Directives; +import graphql.ExecutionResult; +import graphql.Internal; +import graphql.execution.MergedField; +import graphql.execution.ValuesResolver; +import graphql.execution.reactive.SingleSubscriberPublisher; +import graphql.language.Directive; +import graphql.language.Field; +import org.reactivestreams.Publisher; + +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +import static graphql.Directives.*; + +/** + * This provides support for @defer directives on fields that mean that results will be sent AFTER + * the main result is sent via a Publisher stream. + */ +@Internal +public class DeferSupport { + + private final AtomicBoolean deferDetected = new AtomicBoolean(false); + private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); + private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); + private final ValuesResolver valuesResolver = new ValuesResolver(); + + public boolean checkForDeferDirective(MergedField currentField, Map variables) { + for (Field field : currentField.getFields()) { + Directive directive = field.getDirective(DeferDirective.getName()); + if (directive != null) { + Map argumentValues = valuesResolver.getArgumentValues(DeferDirective.getArguments(), directive.getArguments(), variables); + return (Boolean) argumentValues.get("if"); + } + } + return false; + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void drainDeferredCalls() { + if (deferredCalls.isEmpty()) { + publisher.noMoreData(); + return; + } + DeferredCall deferredCall = deferredCalls.pop(); + CompletableFuture future = deferredCall.invoke(); + future.whenComplete((executionResult, exception) -> { + if (exception != null) { + publisher.offerError(exception); + return; + } + publisher.offer(executionResult); + drainDeferredCalls(); + }); + } + + public void enqueue(DeferredCall deferredCall) { + deferDetected.set(true); + deferredCalls.offer(deferredCall); + } + + public boolean isDeferDetected() { + return deferDetected.get(); + } + + /** + * When this is called the deferred execution will begin + * + * @return the publisher of deferred results + */ + public Publisher startDeferredCalls() { + drainDeferredCalls(); + return publisher; + } +} diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java new file mode 100644 index 0000000000..f1da0c8683 --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -0,0 +1,43 @@ +package graphql.execution.defer; + +import graphql.DeferredExecutionResult; +import graphql.DeferredExecutionResultImpl; +import graphql.ExecutionResult; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.execution.ExecutionPath; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * This represents a deferred call (aka @defer) to get an execution result sometime after + * the initial query has returned + */ +@Internal +public class DeferredCall { + private final ExecutionPath path; + private final Supplier> call; + private final DeferredErrorSupport errorSupport; + + public DeferredCall(ExecutionPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { + this.path = path; + this.call = call; + this.errorSupport = deferredErrorSupport; + } + + CompletableFuture invoke() { + CompletableFuture future = call.get(); + return future.thenApply(this::transformToDeferredResult); + } + + private DeferredExecutionResult transformToDeferredResult(ExecutionResult executionResult) { + List errorsEncountered = errorSupport.getErrors(); + DeferredExecutionResultImpl.Builder builder = DeferredExecutionResultImpl.newDeferredExecutionResult().from(executionResult); + return builder + .addErrors(errorsEncountered) + .path(path) + .build(); + } +} diff --git a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java new file mode 100644 index 0000000000..3ef4f34c5d --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java @@ -0,0 +1,31 @@ +package graphql.execution.defer; + +import graphql.ExceptionWhileDataFetching; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.execution.ExecutionStrategyParameters; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This captures errors that occur while a deferred call is being made + */ +@Internal +public class DeferredErrorSupport { + + private final List errors = new CopyOnWriteArrayList<>(); + + public void onFetchingException(ExecutionStrategyParameters parameters, Throwable e) { + ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(parameters.getPath(), e, parameters.getField().getSingleField().getSourceLocation()); + onError(error); + } + + public void onError(GraphQLError gError) { + errors.add(gError); + } + + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 70e7bd063f..697c4e6241 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -10,6 +10,7 @@ import graphql.execution.ExecutionContext; import graphql.execution.FieldValueInfo; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -175,6 +176,16 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument return new ChainedExecutionStrategyInstrumentationContext(mapAndDropNulls(instrumentations, mapper)); } + @Override + public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { + return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() + .map(instrumentation -> { + InstrumentationState state = getState(instrumentation, parameters.getInstrumentationState()); + return instrumentation.beginDeferredField(parameters.withNewState(state)); + }) + .collect(toList())); + } + @Override @NotNull public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { @@ -434,5 +445,28 @@ public void onFieldValuesException() { } } + private static class ChainedDeferredExecutionStrategyInstrumentationContext implements DeferredFieldInstrumentationContext { + + private final List contexts; + + ChainedDeferredExecutionStrategyInstrumentationContext(List contexts) { + this.contexts = Collections.unmodifiableList(contexts); + } + + @Override + public void onDispatched(CompletableFuture result) { + contexts.forEach(context -> context.onDispatched(result)); + } + + @Override + public void onCompleted(ExecutionResult result, Throwable t) { + contexts.forEach(context -> context.onCompleted(result, t)); + } + + @Override + public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + contexts.forEach(context -> context.onFieldValueInfo(fieldValueInfo)); + } + } } diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java new file mode 100644 index 0000000000..39de62be19 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java @@ -0,0 +1,12 @@ +package graphql.execution.instrumentation; + +import graphql.ExecutionResult; +import graphql.execution.FieldValueInfo; + +public interface DeferredFieldInstrumentationContext extends InstrumentationContext { + + default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + + } + +} diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index ae26e360e7..3b3b7277e2 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -5,6 +5,7 @@ import graphql.PublicSpi; import graphql.execution.ExecutionContext; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -222,6 +223,14 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen return beginExecutionStrategy(parameters.withNewState(state)); } + /** + * This is called just before a deferred field is resolved into a value. + * + * @param parameters the parameters to this step + * + * @return a non null {@link InstrumentationContext} object that will be called back when the step ends + */ + DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters); /** * This is called each time a subscription field produces a new reactive stream event value and it needs to be mapped over via the graphql field subselection. diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 87c137e303..475e30d053 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -7,11 +7,13 @@ import graphql.execution.AsyncExecutionStrategy; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategy; +import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -134,6 +136,26 @@ private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContex return state.getApproach().beginExecutionStrategy(parameters, state.getState()); } + @Override + public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { + DataLoaderDispatcherInstrumentationState state = parameters.getInstrumentationState(); + // + // if there are no data loaders, there is nothing to do + // + if (state.hasNoDataLoaders()) { + return new DeferredFieldInstrumentationContext() { + @Override + public void onDispatched(CompletableFuture result) { + } + + @Override + public void onCompleted(ExecutionResult result, Throwable t) { + } + }; + + } + return state.getApproach().beginDeferredField(parameters.withNewState(state.getState())); + } @Override public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 7da689db9a..357bbe2dbb 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -5,15 +5,20 @@ import graphql.Internal; import graphql.execution.FieldValueInfo; import graphql.execution.ResultPath; +import graphql.execution.MergedField; +import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.util.LockKit; import org.dataloader.DataLoaderRegistry; import org.slf4j.Logger; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -184,6 +189,35 @@ private int getCountForList(List fieldValueInfos) { return result; } + DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { + CallStack callStack = parameters.getInstrumentationState(); + int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); + synchronized (callStack) { + callStack.clearAndMarkCurrentLevelAsReady(level); + } + + return new DeferredFieldInstrumentationContext() { + @Override + public void onDispatched(CompletableFuture result) { + + } + + @Override + public void onCompleted(ExecutionResult result, Throwable t) { + } + + @Override + public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + boolean dispatchNeeded; + synchronized (callStack) { + dispatchNeeded = handleOnFieldValuesInfo(Collections.singletonList(fieldValueInfo), callStack, level); + } + if (dispatchNeeded) { + dispatch(); + } + } + }; + } public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { CallStack callStack = (CallStack) rawState; diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java new file mode 100644 index 0000000000..f8a0f47c2e --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java @@ -0,0 +1,41 @@ +package graphql.execution.instrumentation.parameters; + +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.schema.GraphQLFieldDefinition; + +/** + * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods + */ +public class InstrumentationDeferredFieldParameters extends InstrumentationFieldParameters { + + private final ExecutionStrategyParameters executionStrategyParameters; + + public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, GraphQLFieldDefinition fieldDef, ExecutionStepInfo executionStepInfo) { + this(executionContext, executionStrategyParameters, fieldDef, executionStepInfo, executionContext.getInstrumentationState()); + } + + InstrumentationDeferredFieldParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, GraphQLFieldDefinition fieldDef, ExecutionStepInfo executionStepInfo, InstrumentationState instrumentationState) { + super(executionContext, fieldDef, executionStepInfo, instrumentationState); + this.executionStrategyParameters = executionStrategyParameters; + } + + /** + * Returns a cloned parameters object with the new state + * + * @param instrumentationState the new state for this parameters object + * + * @return a new parameters object with the new state + */ + @Override + public InstrumentationDeferredFieldParameters withNewState(InstrumentationState instrumentationState) { + return new InstrumentationDeferredFieldParameters( + this.getExecutionContext(), this.executionStrategyParameters, this.getField(), this.getExecutionStepInfo(), instrumentationState); + } + + public ExecutionStrategyParameters getExecutionStrategyParameters() { + return executionStrategyParameters; + } +} diff --git a/src/main/java/graphql/validation/Validator.java b/src/main/java/graphql/validation/Validator.java index 54558c617a..421fedc7c4 100644 --- a/src/main/java/graphql/validation/Validator.java +++ b/src/main/java/graphql/validation/Validator.java @@ -7,6 +7,9 @@ import graphql.schema.GraphQLSchema; import graphql.validation.rules.ArgumentsOfCorrectType; import graphql.validation.rules.UniqueObjectFieldName; +import graphql.validation.rules.DeferredDirectiveOnNonNullableField; +import graphql.validation.rules.DeferredDirectiveOnQueryOperation; +import graphql.validation.rules.DeferredMustBeOnAllFields; import graphql.validation.rules.ExecutableDefinitions; import graphql.validation.rules.FieldsOnCorrectType; import graphql.validation.rules.FragmentsOnCompositeType; @@ -22,6 +25,7 @@ import graphql.validation.rules.OverlappingFieldsCanBeMerged; import graphql.validation.rules.PossibleFragmentSpreads; import graphql.validation.rules.ProvidedNonNullArguments; +import graphql.validation.rules.ScalarLeafs; import graphql.validation.rules.ScalarLeaves; import graphql.validation.rules.SubscriptionUniqueRootField; import graphql.validation.rules.UniqueArgumentNames; @@ -32,6 +36,8 @@ import graphql.validation.rules.VariableDefaultValuesOfCorrectType; import graphql.validation.rules.VariableTypesMatch; import graphql.validation.rules.VariablesAreInputTypes; +import graphql.validation.rules.UniqueArgumentNamesRule; +import graphql.validation.rules.UniqueVariableNamesRule; import java.util.ArrayList; import java.util.List; @@ -148,6 +154,16 @@ public List createRules(ValidationContext validationContext, Valid UniqueArgumentNames uniqueArgumentNamesRule = new UniqueArgumentNames(validationContext, validationErrorCollector); rules.add(uniqueArgumentNamesRule); + // our extensions beyond spec + DeferredDirectiveOnNonNullableField deferredDirectiveOnNonNullableField = new DeferredDirectiveOnNonNullableField(validationContext, validationErrorCollector); + rules.add(deferredDirectiveOnNonNullableField); + + DeferredDirectiveOnQueryOperation deferredDirectiveOnQueryOperation = new DeferredDirectiveOnQueryOperation(validationContext, validationErrorCollector); + rules.add(deferredDirectiveOnQueryOperation); + + DeferredMustBeOnAllFields deferredMustBeOnAllFields = new DeferredMustBeOnAllFields(validationContext, validationErrorCollector); + rules.add(deferredMustBeOnAllFields); + UniqueVariableNames uniqueVariableNamesRule = new UniqueVariableNames(validationContext, validationErrorCollector); rules.add(uniqueVariableNamesRule); diff --git a/src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java b/src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java new file mode 100644 index 0000000000..680f025c93 --- /dev/null +++ b/src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java @@ -0,0 +1,38 @@ +package graphql.validation.rules; + +import graphql.Directives; +import graphql.Internal; +import graphql.language.Directive; +import graphql.language.Node; +import graphql.schema.GraphQLCompositeType; +import graphql.schema.GraphQLFieldDefinition; +import graphql.validation.AbstractRule; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationErrorCollector; + +import java.util.List; + +@Internal +public abstract class DeferredDirectiveAbstractRule extends AbstractRule { + + public DeferredDirectiveAbstractRule(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + } + + @Override + public void checkDirective(Directive directive, List ancestors) { + if (!directive.getName().equals(Directives.DeferDirective.getName())) { + return; + } + + GraphQLCompositeType parentType = getValidationContext().getParentType(); + GraphQLFieldDefinition fieldDef = getValidationContext().getFieldDef(); + if (parentType == null || fieldDef == null) { + // some previous rule will have caught this + return; + } + onDeferredDirective(directive, ancestors, parentType, fieldDef); + } + + protected abstract void onDeferredDirective(Directive deferredDirective, List ancestors, GraphQLCompositeType parentType, GraphQLFieldDefinition fieldDef); +} diff --git a/src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java b/src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java new file mode 100644 index 0000000000..8de40de2c5 --- /dev/null +++ b/src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java @@ -0,0 +1,32 @@ +package graphql.validation.rules; + +import graphql.Internal; +import graphql.language.Directive; +import graphql.language.Node; +import graphql.schema.GraphQLCompositeType; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLTypeUtil; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationErrorCollector; +import graphql.validation.ValidationErrorType; + +import java.util.List; + +@Internal +public class DeferredDirectiveOnNonNullableField extends DeferredDirectiveAbstractRule { + + + public DeferredDirectiveOnNonNullableField(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + } + + @Override + protected void onDeferredDirective(Directive deferredDirective, List ancestors, GraphQLCompositeType parentType, GraphQLFieldDefinition fieldDef) { + GraphQLOutputType fieldDefType = fieldDef.getType(); + if (!GraphQLTypeUtil.isNullable(fieldDefType)) { + String message = String.format("@defer directives can only be applied to nullable fields - %s.%s is non nullable", parentType.getName(), fieldDef.getName()); + addError(ValidationErrorType.DeferDirectiveOnNonNullField, deferredDirective.getSourceLocation(), message); + } + } +} diff --git a/src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java b/src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java new file mode 100644 index 0000000000..55d7ed0c9a --- /dev/null +++ b/src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java @@ -0,0 +1,41 @@ +package graphql.validation.rules; + +import graphql.Internal; +import graphql.language.Directive; +import graphql.language.Node; +import graphql.language.OperationDefinition; +import graphql.schema.GraphQLCompositeType; +import graphql.schema.GraphQLFieldDefinition; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationErrorCollector; +import graphql.validation.ValidationErrorType; + +import java.util.List; +import java.util.Optional; + +@Internal +public class DeferredDirectiveOnQueryOperation extends DeferredDirectiveAbstractRule { + + public DeferredDirectiveOnQueryOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + } + + @Override + protected void onDeferredDirective(Directive deferredDirective, List ancestors, GraphQLCompositeType parentType, GraphQLFieldDefinition fieldDef) { + Optional operationDefinition = getOperation(ancestors); + if (operationDefinition.isPresent()) { + OperationDefinition.Operation operation = operationDefinition.get().getOperation(); + if (operation != OperationDefinition.Operation.QUERY) { + String message = String.format("@defer directives can only be applied to QUERY operations and not '%s' operations - %s.%s ", operation, parentType.getName(), fieldDef.getName()); + addError(ValidationErrorType.DeferDirectiveNotOnQueryOperation, deferredDirective.getSourceLocation(), message); + } + } + } + + private Optional getOperation(List ancestors) { + return ancestors.stream() + .filter(def -> def instanceof OperationDefinition) + .map((def -> (OperationDefinition) def)) + .findFirst(); + } +} diff --git a/src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java b/src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java new file mode 100644 index 0000000000..3f7b019ec5 --- /dev/null +++ b/src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java @@ -0,0 +1,100 @@ +package graphql.validation.rules; + +import graphql.Directives; +import graphql.Internal; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.InlineFragment; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import graphql.validation.AbstractRule; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationError; +import graphql.validation.ValidationErrorCollector; +import graphql.validation.ValidationErrorType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.groupingBy; + +@Internal +public class DeferredMustBeOnAllFields extends AbstractRule { + + private final Map, List> fieldsByPath = new LinkedHashMap<>(); + + public DeferredMustBeOnAllFields(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + } + + @Override + public void documentFinished(Document document) { + fieldsByPath.forEach((path, allFieldsOnPath) -> { + Map> fieldsByName = allFieldsOnPath.stream() + .collect(groupingBy(Field::getName)); + fieldsByName.forEach((fieldName, fields) -> { + if (fields.size() > 1) { + if (!allFieldsHaveDeferredDirectiveIfAnyOneDoes(fields)) { + recordBadDeferredFields(path, fieldName, fields); + } + } + }); + }); + } + + private boolean allFieldsHaveDeferredDirectiveIfAnyOneDoes(List fields) { + long count = fields.stream() + .filter(fld -> fld.getDirective(Directives.DeferDirective.getName()) != null) + .count(); + if (count == 0) { + return true; + } else { + return count == fields.size(); + } + } + + private void recordBadDeferredFields(List path, String fieldName, List fields) { + String message = String.format("If any of the multiple declarations of a field within the query (via fragments and field selections) contain the @defer directive, then all of them have to contain the @defer directive - field '%s' at '%s'", fieldName, path); + getValidationErrorCollector().addError(new ValidationError(ValidationErrorType.DeferMustBeOnAllFields, fields.get(0).getSourceLocation(), message, path)); + } + + @Override + public void checkSelectionSet(SelectionSet selectionSet) { + List queryPath = getValidationContext().getQueryPath(); + ArrayList> seenSelections = new ArrayList<>(); + addFields(queryPath, selectionSet, seenSelections); + } + + private void addFields(List path, SelectionSet selectionSet, List> seenSelections) { + if (path == null) { + path = Collections.emptyList(); + } + for (Selection selection : selectionSet.getSelections()) { + // sett issue 1817 - an illegal circular query could cause a stack overflow - protected against it + if (seenSelections.contains(selection)) { + break; + } + seenSelections.add(selection); + if (selection instanceof Field) { + List fields = fieldsByPath.getOrDefault(path, new ArrayList<>()); + fields.add((Field) selection); + fieldsByPath.put(path, fields); + } + if (selection instanceof InlineFragment) { + addFields(path, ((InlineFragment) selection).getSelectionSet(), seenSelections); + } + if (selection instanceof FragmentSpread) { + FragmentSpread fragmentSpread = (FragmentSpread) selection; + FragmentDefinition fragmentDefinition = getValidationContext().getFragment(fragmentSpread.getName()); + if (fragmentDefinition != null) { + addFields(path, fragmentDefinition.getSelectionSet(), seenSelections); + } + } + } + } +} diff --git a/src/test/groovy/example/http/DeferHttpSupport.java b/src/test/groovy/example/http/DeferHttpSupport.java new file mode 100644 index 0000000000..23f2c3dedf --- /dev/null +++ b/src/test/groovy/example/http/DeferHttpSupport.java @@ -0,0 +1,115 @@ +package example.http; + +import graphql.DeferredExecutionResult; +import graphql.ExecutionResult; +import graphql.GraphQL; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.util.Map; + +public class DeferHttpSupport { + + private static final String CRLF = "\r\n"; + + @SuppressWarnings("unchecked") + static void sendDeferredResponse(HttpServletResponse response, ExecutionResult executionResult, Map extensions) { + Publisher deferredResults = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS); + try { + sendMultipartResponse(response, executionResult, deferredResults); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + static private void sendMultipartResponse(HttpServletResponse response, ExecutionResult executionResult, Publisher deferredResults) { + // this implements this apollo defer spec: https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md + // the spec says CRLF + "-----" + CRLF is needed at the end, but it works without it and with it we get client + // side errors with it, so we skp it + response.setStatus(HttpServletResponse.SC_OK); + + response.setHeader("Content-Type", "multipart/mixed; boundary=\"-\""); + response.setHeader("Connection", "keep-alive"); + + // send the first "un deferred" part of the result + writeAndFlushPart(response, executionResult.toSpecification()); + + // now send each deferred part which is given to us as a reactive stream + // of deferred values + deferredResults.subscribe(new Subscriber() { + Subscription subscription; + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + s.request(1); + } + + @Override + public void onNext(DeferredExecutionResult deferredExecutionResult) { + subscription.request(1); + + writeAndFlushPart(response, deferredExecutionResult.toSpecification()); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(System.err); + } + + @Override + public void onComplete() { + } + }); + + } + + private static void writeAndFlushPart(HttpServletResponse response, Map result) { + DeferMultiPart deferMultiPart = new DeferMultiPart(result); + StringBuilder sb = new StringBuilder(); + sb.append(CRLF).append("---").append(CRLF); + String body = deferMultiPart.write(); + sb.append(body); + writeAndFlush(response, sb); + } + + private static void writeAndFlush(HttpServletResponse response, StringBuilder sb) { + try { + PrintWriter writer = response.getWriter(); + writer.write(sb.toString()); + writer.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + + private static class DeferMultiPart { + + private Object body; + + public DeferMultiPart(Object data) { + this.body = data; + } + + public String write() { + StringBuilder result = new StringBuilder(); + String bodyString = bodyToString(); + result.append("Content-Type: application/json").append(CRLF); + result.append("Content-Length: ").append(bodyString.length()).append(CRLF).append(CRLF); + result.append(bodyString); + return result.toString(); + } + + private String bodyToString() { + return JsonKit.GSON.toJson(body); + } + } + +} diff --git a/src/test/groovy/example/http/HttpMain.java b/src/test/groovy/example/http/HttpMain.java index 4f3b7c8936..0b84f96e72 100644 --- a/src/test/groovy/example/http/HttpMain.java +++ b/src/test/groovy/example/http/HttpMain.java @@ -160,6 +160,10 @@ private void handleStarWars(HttpServletRequest httpRequest, HttpServletResponse private void returnAsJson(HttpServletResponse response, ExecutionResult executionResult) throws IOException { + Map extensions = executionResult.getExtensions(); + if (extensions != null && extensions.containsKey(GraphQL.DEFERRED_RESULTS)) { + DeferHttpSupport.sendDeferredResponse(response, executionResult, extensions); + } sendNormalResponse(response, executionResult); } diff --git a/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy b/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy new file mode 100644 index 0000000000..e1f9be0cc5 --- /dev/null +++ b/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy @@ -0,0 +1,35 @@ +package graphql.execution.defer + +import graphql.DeferredExecutionResult +import graphql.ExecutionResult +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +import java.util.concurrent.atomic.AtomicBoolean + +class BasicSubscriber implements Subscriber { + Subscription subscription + AtomicBoolean finished = new AtomicBoolean() + Throwable throwable + + @Override + void onSubscribe(Subscription s) { + assert s != null, "subscription must not be null" + this.subscription = s + s.request(1) + } + + @Override + void onNext(DeferredExecutionResult executionResult) { + } + + @Override + void onError(Throwable t) { + finished.set(true) + } + + @Override + void onComplete() { + finished.set(true) + } +} diff --git a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy new file mode 100644 index 0000000000..7245f24ee9 --- /dev/null +++ b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy @@ -0,0 +1,45 @@ +package graphql.execution.defer + +import graphql.DeferredExecutionResult +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +import java.util.concurrent.atomic.AtomicBoolean + +class CapturingSubscriber implements Subscriber { + Subscription subscription + AtomicBoolean finished = new AtomicBoolean() + Throwable throwable + List executionResults = [] + List executionResultData = [] + + AtomicBoolean subscribeTo(Publisher publisher) { + publisher.subscribe(this) + return finished + } + + @Override + void onSubscribe(Subscription s) { + assert s != null, "subscription must not be null" + this.subscription = s + s.request(1) + } + + @Override + void onNext(DeferredExecutionResult executionResult) { + executionResults.add(executionResult) + executionResultData.add(executionResult.getData()) + subscription.request(1) + } + + @Override + void onError(Throwable t) { + finished.set(true) + } + + @Override + void onComplete() { + finished.set(true) + } +} diff --git a/src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy new file mode 100644 index 0000000000..6a1a6b8d70 --- /dev/null +++ b/src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy @@ -0,0 +1,328 @@ +package graphql.execution.defer + +import graphql.DeferredExecutionResult +import graphql.ErrorType +import graphql.Directives +import graphql.ExecutionInput +import graphql.ExecutionResult +import graphql.GraphQL +import graphql.TestUtil +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.idl.RuntimeWiring +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import org.awaitility.Awaitility +import org.reactivestreams.Publisher +import spock.lang.Specification + +import java.time.Duration +import java.util.concurrent.CompletableFuture + +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring + +class DeferSupportIntegrationTest extends Specification { + def then = 0 + + def sentAt() { + def seconds = Duration.ofMillis(System.currentTimeMillis() - then).toMillis() + "T+" + seconds + } + + def sleepSome(DataFetchingEnvironment env) { + Integer sleepTime = env.getArgument("sleepTime") + sleepTime = Optional.ofNullable(sleepTime).orElse(0) + Thread.sleep(sleepTime) + } + + def schemaSpec = ''' + type Query { + post : Post + mandatoryReviews : [Review]! + } + + type Mutation { + mutate(arg : String) : String + } + + type Post { + postText : String + sentAt : String + echo(text : String = "echo") : String + comments(sleepTime : Int, prefix :String) : [Comment] + reviews(sleepTime : Int) : [Review] + } + + type Comment { + commentText : String + sentAt : String + comments(sleepTime : Int, prefix :String) : [Comment] + goes : Bang + } + + type Review { + reviewText : String + sentAt : String + comments(sleepTime : Int, prefix :String) : [Comment] + goes : Bang + } + + type Bang { + bang : String + } + ''' + + DataFetcher postFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) { + return CompletableFuture.supplyAsync({ + [postText: "post_data", sentAt: sentAt()] + }) + } + } + DataFetcher commentsFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment env) { + return CompletableFuture.supplyAsync({ + sleepSome(env) + + def prefix = env.getArgument("prefix") + prefix = prefix == null ? "" : prefix + + def result = [] + for (int i = 0; i < 3; i++) { + result.add([commentText: prefix + "comment" + i, sentAt: sentAt(), goes: "goes"]) + } + return result + }) + } + + } + DataFetcher reviewsFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment env) { + return CompletableFuture.supplyAsync({ + sleepSome(env) + def result = [] + for (int i = 0; i < 3; i++) { + result.add([reviewText: "review" + i, sentAt: sentAt(), goes: "goes"]) + } + return result + }) + } + } + + DataFetcher bangDataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) { + throw new RuntimeException("Bang!") + } + } + DataFetcher echoDataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) { + return environment.getArgument("text") + } + } + + GraphQL graphQL = null + + void setup() { + then = System.currentTimeMillis() + + def runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(newTypeWiring("Query").dataFetcher("post", postFetcher)) + .type(newTypeWiring("Post").dataFetcher("comments", commentsFetcher)) + .type(newTypeWiring("Post").dataFetcher("echo", echoDataFetcher)) + .type(newTypeWiring("Post").dataFetcher("reviews", reviewsFetcher)) + .type(newTypeWiring("Bang").dataFetcher("bang", bangDataFetcher)) + + .type(newTypeWiring("Comment").dataFetcher("comments", commentsFetcher)) + .type(newTypeWiring("Review").dataFetcher("comments", commentsFetcher)) + .build() + + def schema = TestUtil.schema(schemaSpec, runtimeWiring) + .transform({ builder -> builder.additionalDirective(Directives.DeferDirective) }) + this.graphQL = GraphQL.newGraphQL(schema).build() + } + + def "test defer support end to end"() { + + def query = ''' + query { + post { + postText + + a :comments(sleepTime:200) @defer { + commentText + } + + b : reviews(sleepTime:100) @defer { + reviewText + comments(prefix : "b_") @defer { + commentText + } + } + + c: reviews @defer { + goes { + bang + } + } + } + } + ''' + + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + + then: + initialResult.errors.isEmpty() + initialResult.data == ["post": ["postText": "post_data", a: null, b: null, c: null]] + + when: + + Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream) + Awaitility.await().untilTrue(subscriber.finished) + + List resultList = subscriber.executionResults + + then: + + assertDeferredData(resultList) + } + + def "test defer support keeps the fields named correctly when interspersed in the query"() { + + def query = ''' + query { + post { + interspersedA: echo(text:"before a:") + + a: comments(sleepTime:200) @defer { + commentText + } + + interspersedB: echo(text:"before b:") + + b : reviews(sleepTime:100) @defer { + reviewText + comments(prefix : "b_") @defer { + commentText + } + } + + interspersedC: echo(text:"before c:") + + c: reviews @defer { + goes { + bang + } + } + + interspersedD: echo(text:"after c:") + } + } + ''' + + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + + then: + initialResult.errors.isEmpty() + initialResult.data == ["post": [ + "interspersedA": "before a:", + "a" : null, + "interspersedB": "before b:", + "b" : null, + "interspersedC": "before c:", + "c" : null, + "interspersedD": "after c:", + ]] + + when: + + Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream); + Awaitility.await().untilTrue(subscriber.finished) + + List resultList = subscriber.executionResults + + then: + + assertDeferredData(resultList) + } + + def assertDeferredData(ArrayList resultList) { + resultList.size() == 6 + + assert resultList[0].data == [[commentText: "comment0"], [commentText: "comment1"], [commentText: "comment2"]] + assert resultList[0].errors == [] + assert resultList[0].path == ["post", "a"] + + assert resultList[1].data == [[reviewText: "review0", comments: null], [reviewText: "review1", comments: null], [reviewText: "review2", comments: null]] + assert resultList[1].errors == [] + assert resultList[1].path == ["post", "b"] + + // exceptions in here + assert resultList[2].errors.size() == 3 + assert resultList[2].errors[0].getMessage() == "Exception while fetching data (/post/c[0]/goes/bang) : Bang!" + assert resultList[2].errors[1].getMessage() == "Exception while fetching data (/post/c[1]/goes/bang) : Bang!" + assert resultList[2].errors[2].getMessage() == "Exception while fetching data (/post/c[2]/goes/bang) : Bang!" + assert resultList[2].path == ["post", "c"] + + // sub defers are sent in encountered order + assert resultList[3].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] + assert resultList[3].errors == [] + assert resultList[3].path == ["post", "b", 0, "comments"] + + assert resultList[4].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] + assert resultList[4].errors == [] + assert resultList[4].path == ["post", "b", 1, "comments"] + + assert resultList[5].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] + assert resultList[5].errors == [] + assert resultList[5].path == ["post", "b", 2, "comments"] + + true + } + + def "nonNull types are not allowed"() { + + def query = ''' + { + mandatoryReviews @defer # nulls are not allowed + { + reviewText + } + } + ''' + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + then: + initialResult.errors.size() == 1 + initialResult.errors[0].errorType == ErrorType.ValidationError + (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveOnNonNullField + + } + + def "mutations cant have defers"() { + + def query = ''' + mutation { + mutate(arg : "go") @defer + } + ''' + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + then: + initialResult.errors.size() == 1 + initialResult.errors[0].errorType == ErrorType.ValidationError + (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveNotOnQueryOperation + } +} diff --git a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy new file mode 100644 index 0000000000..7f2c41e364 --- /dev/null +++ b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy @@ -0,0 +1,272 @@ +package graphql.execution.defer + +import graphql.DeferredExecutionResult +import graphql.ExecutionResult +import graphql.ExecutionResultImpl +import graphql.execution.ExecutionPath +import graphql.language.Argument +import graphql.language.BooleanValue +import graphql.language.Directive +import graphql.language.Field +import graphql.language.VariableReference +import org.awaitility.Awaitility +import spock.lang.Specification + +import java.util.concurrent.CompletableFuture + +import static graphql.TestUtil.mergedField + +class DeferSupportTest extends Specification { + + + def "emits N deferred calls with order preserved"() { + + given: + def deferSupport = new DeferSupport() + deferSupport.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + deferSupport.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + + when: + List results = [] + def subscriber = new BasicSubscriber() { + @Override + void onNext(DeferredExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + } + deferSupport.startDeferredCalls().subscribe(subscriber) + Awaitility.await().untilTrue(subscriber.finished) + then: + + results.size() == 3 + results[0].data == "A" + results[1].data == "B" + results[2].data == "C" + } + + def "calls within calls are enqueued correctly"() { + given: + def deferSupport = new DeferSupport() + deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "A", "a", 100, "/a")) + deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "B", "b", 50, "/b")) + deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "C", "c", 10, "/c")) + + when: + List results = [] + BasicSubscriber subscriber = new BasicSubscriber() { + @Override + void onNext(DeferredExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + } + deferSupport.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + then: + + results.size() == 6 + results[0].data == "A" + results[1].data == "B" + results[2].data == "C" + results[3].data == "a" + results[4].data == "b" + results[5].data == "c" + } + + def "stops at first exception encountered"() { + given: + def deferSupport = new DeferSupport() + deferSupport.enqueue(offThread("A", 100, "/field/path")) + deferSupport.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception + deferSupport.enqueue(offThread("C", 10, "/field/path")) + + when: + List results = [] + Throwable thrown = null + def subscriber = new BasicSubscriber() { + @Override + void onNext(DeferredExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + + @Override + void onError(Throwable t) { + thrown = t + finished.set(true) + } + + @Override + void onComplete() { + assert false, "This should not be called!" + } + } + deferSupport.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + then: + + thrown.message == "java.lang.RuntimeException: Bang" + } + + def "you can cancel the subscription"() { + given: + def deferSupport = new DeferSupport() + deferSupport.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + deferSupport.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + + when: + List results = [] + def subscriber = new BasicSubscriber() { + @Override + void onNext(DeferredExecutionResult executionResult) { + results.add(executionResult) + subscription.cancel() + finished.set(true) + } + } + deferSupport.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + then: + + results.size() == 1 + results[0].data == "A" + + } + + def "you cant subscribe twice"() { + given: + def deferSupport = new DeferSupport() + deferSupport.enqueue(offThread("A", 100, "/field/path")) + deferSupport.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second + deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + + when: + Throwable expectedThrowble + deferSupport.startDeferredCalls().subscribe(new BasicSubscriber()) + deferSupport.startDeferredCalls().subscribe(new BasicSubscriber() { + @Override + void onError(Throwable t) { + expectedThrowble = t + } + }) + then: + expectedThrowble != null + } + + def "indicates of there any defers present"() { + given: + def deferSupport = new DeferSupport() + + when: + def deferPresent1 = deferSupport.isDeferDetected() + + then: + !deferPresent1 + + when: + deferSupport.enqueue(offThread("A", 100, "/field/path")) + def deferPresent2 = deferSupport.isDeferDetected() + + then: + deferPresent2 + } + + def "detects @defer directive"() { + given: + def deferSupport = new DeferSupport() + + when: + def noDirectivePresent = deferSupport.checkForDeferDirective(mergedField([ + new Field("a"), + new Field("b") + ]), [:]) + + then: + !noDirectivePresent + + when: + def directivePresent = deferSupport.checkForDeferDirective(mergedField([ + Field.newField("a").directives([new Directive("defer")]).build(), + new Field("b") + ]), [:]) + + then: + directivePresent + } + + def "detects @defer directive can be controlled via if"() { + given: + def deferSupport = new DeferSupport() + + when: + def ifArg = new Argument("if", new BooleanValue(false)) + def directivePresent = deferSupport.checkForDeferDirective(mergedField([ + Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), + new Field("b") + ]), [:]) + + then: + !directivePresent + + when: + ifArg = new Argument("if", new BooleanValue(true)) + directivePresent = deferSupport.checkForDeferDirective(mergedField([ + Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), + new Field("b") + ]), [:]) + + then: + directivePresent + + when: + ifArg = new Argument("if", new VariableReference("varRef")) + directivePresent = deferSupport.checkForDeferDirective(mergedField([ + Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), + new Field("b") + ]), [varRef: false]) + + then: + !directivePresent + + when: + ifArg = new Argument("if", new VariableReference("varRef")) + directivePresent = deferSupport.checkForDeferDirective(mergedField([ + Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), + new Field("b") + ]), [varRef: true]) + + then: + directivePresent + } + + private static DeferredCall offThread(String data, int sleepTime, String path) { + def callSupplier = { + CompletableFuture.supplyAsync({ + Thread.sleep(sleepTime) + if (data == "Bang") { + throw new RuntimeException(data) + } + new ExecutionResultImpl(data, []) + }) + } + return new DeferredCall(ExecutionPath.parse(path), callSupplier, new DeferredErrorSupport()) + } + + private + static DeferredCall offThreadCallWithinCall(DeferSupport deferSupport, String dataParent, String dataChild, int sleepTime, String path) { + def callSupplier = { + CompletableFuture.supplyAsync({ + Thread.sleep(sleepTime) + deferSupport.enqueue(offThread(dataChild, sleepTime, path)) + new ExecutionResultImpl(dataParent, []) + }) + } + return new DeferredCall(ExecutionPath.parse("/field/path"), callSupplier, new DeferredErrorSupport()) + } +} diff --git a/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy b/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy new file mode 100644 index 0000000000..2e703bc875 --- /dev/null +++ b/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy @@ -0,0 +1,48 @@ +package graphql.execution.defer + + +import graphql.ExecutionResultImpl +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import spock.lang.Specification + +import static graphql.execution.ExecutionPath.parse +import static java.util.concurrent.CompletableFuture.completedFuture + +class DeferredCallTest extends Specification { + + def "test call capture gives a CF"() { + given: + DeferredCall call = new DeferredCall(parse("/path"), { + completedFuture(new ExecutionResultImpl("some data", Collections.emptyList())) + }, new DeferredErrorSupport()) + + when: + def future = call.invoke() + then: + future.join().data == "some data" + future.join().path == ["path"] + } + + def "test error capture happens via CF"() { + given: + def errorSupport = new DeferredErrorSupport() + errorSupport.onError(new ValidationError(ValidationErrorType.MissingFieldArgument)) + errorSupport.onError(new ValidationError(ValidationErrorType.FieldsConflict)) + + DeferredCall call = new DeferredCall(parse("/path"), { + completedFuture(new ExecutionResultImpl("some data", [new ValidationError(ValidationErrorType.FieldUndefined)])) + }, errorSupport) + + when: + def future = call.invoke() + def er = future.join() + + then: + er.errors.size() == 3 + er.errors[0].message.contains("Validation error of type FieldUndefined") + er.errors[1].message.contains("Validation error of type MissingFieldArgument") + er.errors[2].message.contains("Validation error of type FieldsConflict") + er.path == ["path"] + } +} diff --git a/src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy b/src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy new file mode 100644 index 0000000000..3fc8e8e1a8 --- /dev/null +++ b/src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy @@ -0,0 +1,77 @@ +package graphql.execution.defer + +import graphql.DeferredExecutionResult +import graphql.Directives +import graphql.ExecutionResult +import graphql.GraphQL +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.reactivestreams.Publisher +import spock.lang.Specification + +import static graphql.TestUtil.schema +import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring +import static org.awaitility.Awaitility.await + +class DeferredErrorSupportTest extends Specification { + + def "#1040 errors in stage one do not affect deferred stages"() { + + def spec = ''' + type Query { + stage1 : String + stage2 : String + } + ''' + + def bangDF = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) { + throw new RuntimeException("bang-" + environment.getField().getName()) + } + } + + def runtimeWiring = newRuntimeWiring().type( + newTypeWiring("Query") + .dataFetchers([ + stage1: bangDF, + stage2: bangDF, + ]) + ).build() + + def schema = schema(spec, runtimeWiring).transform({ b -> b.additionalDirective(Directives.DeferDirective) }) + def graphql = GraphQL.newGraphQL(schema).build() + + when: + def executionResult = graphql.execute(''' + { + stage1, + stage2 @defer + } + ''') + + then: + executionResult.errors.size() == 1 + executionResult.errors[0].getMessage().contains("bang-stage1") + + when: + def executionResultDeferred = null + def subscriber = new BasicSubscriber() { + @Override + void onNext(DeferredExecutionResult executionResultStreamed) { + executionResultDeferred = executionResultStreamed + subscription.request(1) + } + } + Publisher deferredResultStream = executionResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher + deferredResultStream.subscribe(subscriber) + + await().untilTrue(subscriber.finished) + + then: + executionResultDeferred.errors.size() == 1 + executionResultDeferred.errors[0].getMessage().contains("bang-stage2") + + } +} diff --git a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy index e8a9478cb6..0417d7b797 100644 --- a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy @@ -3,6 +3,7 @@ package graphql.execution.instrumentation import graphql.ExecutionInput import graphql.ExecutionResult import graphql.execution.ExecutionContext +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters @@ -67,6 +68,12 @@ class LegacyTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("execute-operation", executionList, throwableList, useOnDispatch) } + @Override + DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { + assert parameters.getInstrumentationState() == instrumentationState + return new TestingInstrumentContext("deferred-field-$parameters.field.name", executionList, throwableList) + } + @Override InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index 4565db0615..0df6c7a5ca 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -1,14 +1,23 @@ package graphql.execution.instrumentation.dataloader - +import graphql.DeferredExecutionResult import graphql.ExecutionInput import graphql.GraphQL +import graphql.execution.defer.CapturingSubscriber import graphql.execution.instrumentation.Instrumentation +import org.awaitility.Awaitility import org.dataloader.DataLoaderRegistry +import org.reactivestreams.Publisher import spock.lang.Specification +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialExpensiveDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedListOfDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getQuery @@ -87,4 +96,57 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() <= 2 } + def "data loader will work with deferred queries"() { + + when: + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(deferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() + def result = graphQL.execute(executionInput) + + Map extensions = result.getExtensions() + Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream) + Awaitility.await().untilTrue(subscriber.finished) + + + then: + + result.data == expectedInitialDeferredData + + subscriber.executionResultData == expectedListOfDeferredData + + // + // with deferred results, we don't achieve the same efficiency + batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 + batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 + } + + def "data loader will work with deferred queries on multiple levels deep"() { + + when: + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveDeferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() + def result = graphQL.execute(executionInput) + + Map extensions = result.getExtensions() + Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream) + Awaitility.await().untilTrue(subscriber.finished) + + + then: + + result.data == expectedInitialExpensiveDeferredData + + subscriber.executionResultData == expectedExpensiveDeferredData + + // + // with deferred results, we don't achieve the same efficiency + batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 + batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 + } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 7b1bd96d54..7d15890a82 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -1,16 +1,25 @@ package graphql.execution.instrumentation.dataloader - +import graphql.DeferredExecutionResult import graphql.ExecutionInput import graphql.GraphQL +import graphql.execution.defer.CapturingSubscriber import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation +import org.awaitility.Awaitility import org.dataloader.DataLoaderRegistry +import org.reactivestreams.Publisher import spock.lang.Ignore import spock.lang.Specification +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialExpensiveDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedListOfDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getQuery @@ -96,5 +105,57 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() <= 2 } + def "chainedInstrumentation: data loader will work with deferred queries"() { + + when: + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(deferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() + def result = graphQL.execute(executionInput) + + Map extensions = result.getExtensions() + Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream) + Awaitility.await().untilTrue(subscriber.finished) + + + then: + + result.data == expectedInitialDeferredData + + subscriber.executionResultData == expectedListOfDeferredData + + // + // with deferred results, we don't achieve the same efficiency + batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 + batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 + } + def "chainedInstrumentation: data loader will work with deferred queries on multiple levels deep"() { + + when: + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveDeferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() + def result = graphQL.execute(executionInput) + + Map extensions = result.getExtensions() + Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream) + Awaitility.await().untilTrue(subscriber.finished) + + + then: + + result.data == expectedInitialExpensiveDeferredData + + subscriber.executionResultData == expectedExpensiveDeferredData + + // + // with deferred results, we don't achieve the same efficiency + batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 + batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 + } } diff --git a/src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy new file mode 100644 index 0000000000..1e1947d443 --- /dev/null +++ b/src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy @@ -0,0 +1,40 @@ +package graphql.validation.rules + +import graphql.Directives +import graphql.language.Directive +import graphql.validation.ValidationContext +import graphql.validation.ValidationErrorCollector +import spock.lang.Specification + +class DeferredDirectiveAbstractRuleTest extends Specification { + ValidationContext validationContext = Mock(ValidationContext) + ValidationErrorCollector errorCollector = new ValidationErrorCollector() + DeferredDirectiveOnNonNullableField deferredRule = new DeferredDirectiveOnNonNullableField(validationContext, errorCollector) + + + def 'if directive name is not deferred then empty errors'() { + when: + deferredRule.checkDirective(new Directive("someOtherName"), []) + then: + errorCollector.errors.isEmpty() + } + + def 'if parent type is empty then empty errors'() { + validationContext.getParentType() >> null + + when: + deferredRule.checkDirective(new Directive(Directives.DeferDirective.name), []) + then: + errorCollector.errors.isEmpty() + } + + def 'if field def is empty then empty errors'() { + validationContext.getFieldDef() >> null + + when: + deferredRule.checkDirective(new Directive(Directives.DeferDirective.name), []) + then: + errorCollector.errors.isEmpty() + } + +} diff --git a/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy new file mode 100644 index 0000000000..fa3f69f46e --- /dev/null +++ b/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy @@ -0,0 +1,46 @@ +package graphql.validation.rules + +import graphql.Directives +import graphql.StarWarsSchema +import graphql.language.Directive +import graphql.validation.ValidationContext +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorCollector +import graphql.validation.ValidationErrorType +import spock.lang.Specification + +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLNonNull.nonNull + +class DeferredDirectiveOnNonNullableFieldTest extends Specification { + ValidationContext validationContext = Mock(ValidationContext) + ValidationErrorCollector errorCollector = new ValidationErrorCollector() + DeferredDirectiveOnNonNullableField deferredOnNullableField = new DeferredDirectiveOnNonNullableField(validationContext, errorCollector) + + + def "denies non nullable fields"() { + def fieldDefinition = newFieldDefinition().name("field").type(nonNull(GraphQLString)).build() + + validationContext.getParentType() >> StarWarsSchema.humanType + validationContext.getFieldDef() >> fieldDefinition + + when: + deferredOnNullableField.checkDirective(new Directive(Directives.DeferDirective.name), []) + then: + !errorCollector.errors.isEmpty() + (errorCollector.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveOnNonNullField + } + + def "allows nullable fields"() { + def fieldDefinition = newFieldDefinition().name("field").type(GraphQLString).build() + + validationContext.getParentType() >> StarWarsSchema.humanType + validationContext.getFieldDef() >> fieldDefinition + + when: + deferredOnNullableField.checkDirective(new Directive(Directives.DeferDirective.name), []) + then: + errorCollector.errors.isEmpty() + } +} diff --git a/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy new file mode 100644 index 0000000000..721fb2c89b --- /dev/null +++ b/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy @@ -0,0 +1,71 @@ +package graphql.validation.rules + +import graphql.StarWarsSchema +import graphql.language.Document +import graphql.parser.Parser +import graphql.validation.LanguageTraversal +import graphql.validation.RulesVisitor +import graphql.validation.TraversalContext +import graphql.validation.ValidationContext +import graphql.validation.ValidationErrorCollector +import graphql.validation.ValidationErrorType +import spock.lang.Specification + +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLNonNull.nonNull + +class DeferredDirectiveOnQueryOperationTest extends Specification { + + ValidationContext validationContext = Mock(ValidationContext) + ValidationErrorCollector errorCollector = new ValidationErrorCollector() + DeferredDirectiveOnQueryOperation directiveOnQueryOperation = new DeferredDirectiveOnQueryOperation(validationContext, errorCollector) + + void setup() { + def traversalContext = Mock(TraversalContext) + validationContext.getSchema() >> StarWarsSchema.starWarsSchema + validationContext.getTraversalContext() >> traversalContext + + def fieldDefinition = newFieldDefinition().name("field").type(nonNull(GraphQLString)).build() + + validationContext.getParentType() >> StarWarsSchema.humanType + validationContext.getFieldDef() >> fieldDefinition + } + + def "denies operations that are not queries"() { + given: + def query = """ + mutation Foo { + name @defer + } + """ + + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [directiveOnQueryOperation])) + + then: + errorCollector.containsValidationError(ValidationErrorType.DeferDirectiveNotOnQueryOperation) + } + + def "allows operations that are queries"() { + given: + def query = """ + query Foo { + name @defer + } + """ + + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [directiveOnQueryOperation])) + + then: + errorCollector.errors.isEmpty() + } + +} diff --git a/src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy new file mode 100644 index 0000000000..9a7c3dcfe5 --- /dev/null +++ b/src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy @@ -0,0 +1,193 @@ +package graphql.validation.rules + +import graphql.Directives +import graphql.GraphQL +import graphql.TestUtil +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import spock.lang.Specification + +class DeferredMustBeOnAllFieldsTest extends Specification { + + def schema = TestUtil.schema(''' + type Query { + newsFeed : NewsFeed + } + + type NewsFeed { + stories : [Story] + } + + type Story { + id : ID + text : String + } + + ''').transform({ it.additionalDirective(Directives.DeferDirective) }) + + + def "all fields MUST contain @defer on all declarations"() { + + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = """ + fragment StoryDetail on Story { + id + text @defer + } + query { + newsFeed { + stories { + text @defer + + # fragment + ...StoryDetail + + ## inline fragment + ... on Story { + id + text # @defer is missing + } + } + } + } + """ + + + when: + def er = graphQL.execute(query) + then: + er.errors.size() == 1 + (er.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferMustBeOnAllFields + (er.errors[0] as ValidationError).queryPath == ["newsFeed", "stories"] + + when: + query = """ + fragment StoryDetail on Story { + id + text # @defer is missing + } + query { + newsFeed { + stories { + text @defer + + # fragment + ...StoryDetail + + ## inline fragment + ... on Story { + id + text @defer + } + } + } + } + """ + + er = graphQL.execute(query) + + then: + er.errors.size() == 1 + (er.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferMustBeOnAllFields + (er.errors[0] as ValidationError).queryPath == ["newsFeed", "stories"] + + when: + query = """ + fragment StoryDetail on Story { + id + text @defer + } + query { + newsFeed { + stories { + text # @defer is missing + + # fragment + ...StoryDetail + + ## inline fragment + ... on Story { + id + text @defer + } + } + } + } + """ + + er = graphQL.execute(query) + + then: + er.errors.size() == 1 + (er.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferMustBeOnAllFields + (er.errors[0] as ValidationError).queryPath == ["newsFeed", "stories"] + + } + + def "if all fields contain @defer then its ok"() { + + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = """ + fragment StoryDetail on Story { + id + text @defer + } + query { + newsFeed { + stories { + text @defer + + # fragment + ...StoryDetail + + ## inline fragment + ... on Story { + id + text @defer + } + } + } + } + """ + + + when: + def er = graphQL.execute(query) + then: + er.errors.size() == 0 + } + + def "if only one field contain @defer then its ok"() { + + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = """ + fragment StoryDetail on Story { + id + text @defer + } + query { + newsFeed { + stories { + id + + # fragment + ...StoryDetail + + ## inline fragment + ... on Story { + id + } + } + } + } + """ + + when: + def er = graphQL.execute(query) + then: + er.errors.size() == 0 + } +} diff --git a/src/test/groovy/readme/DeferredExamples.java b/src/test/groovy/readme/DeferredExamples.java new file mode 100644 index 0000000000..23aff5d472 --- /dev/null +++ b/src/test/groovy/readme/DeferredExamples.java @@ -0,0 +1,96 @@ +package readme; + +import graphql.DeferredExecutionResult; +import graphql.Directives; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.schema.GraphQLSchema; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +@SuppressWarnings({"unused", "ConstantConditions", "UnusedAssignment", "unchecked"}) +public class DeferredExamples { + + GraphQLSchema buildSchemaWithDirective() { + + GraphQLSchema schema = buildSchema(); + schema = schema.transform(builder -> + builder.additionalDirective(Directives.DeferDirective) + ); + return schema; + } + + void basicExample(HttpServletResponse httpServletResponse, String deferredQuery) { + GraphQLSchema schema = buildSchemaWithDirective(); + GraphQL graphQL = GraphQL.newGraphQL(schema).build(); + + // + // deferredQuery contains the query with @defer directives in it + // + ExecutionResult initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(deferredQuery).build()); + + // + // then initial results happen first, the deferred ones will begin AFTER these initial + // results have completed + // + sendMultipartHttpResult(httpServletResponse, initialResult); + + Map extensions = initialResult.getExtensions(); + Publisher deferredResults = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS); + + // + // you subscribe to the deferred results like any other reactive stream + // + deferredResults.subscribe(new Subscriber() { + + Subscription subscription; + + @Override + public void onSubscribe(Subscription s) { + subscription = s; + // + // how many you request is up to you + subscription.request(10); + } + + @Override + public void onNext(DeferredExecutionResult executionResult) { + // + // as each deferred result arrives, send it to where it needs to go + // + sendMultipartHttpResult(httpServletResponse, executionResult); + subscription.request(10); + } + + @Override + public void onError(Throwable t) { + handleError(httpServletResponse, t); + } + + @Override + public void onComplete() { + completeResponse(httpServletResponse); + } + }); + } + + private void completeResponse(HttpServletResponse httpServletResponse) { + } + + private void handleError(HttpServletResponse httpServletResponse, Throwable t) { + } + + private void sendMultipartHttpResult(HttpServletResponse httpServletResponse, ExecutionResult initialResult) { + } + + + private GraphQLSchema buildSchema() { + return null; + } + +} From c7088b1273ad5df0331638541dc22059dfa5f8b8 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 15 Jan 2024 14:37:09 +1100 Subject: [PATCH 099/393] Fix compilation errors --- .../graphql/DeferredExecutionResultImpl.java | 11 +- src/main/java/graphql/execution/Async.java | 10 ++ .../execution/AsyncExecutionStrategy.java | 28 +++-- .../graphql/execution/ExecutionContext.java | 2 - .../graphql/execution/ExecutionStrategy.java | 12 +- .../graphql/execution/defer/DeferSupport.java | 21 ++-- .../graphql/execution/defer/DeferredCall.java | 6 +- .../ChainedInstrumentation.java | 63 +++++----- ...ecutionStrategyInstrumentationContext.java | 5 + .../instrumentation/Instrumentation.java | 4 +- .../DataLoaderDispatcherInstrumentation.java | 6 +- .../FieldLevelTrackingApproach.java | 5 +- ...nstrumentationDeferredFieldParameters.java | 24 +--- .../java/graphql/validation/Validator.java | 18 +-- .../DeferredDirectiveOnNonNullableField.java | 32 ----- .../DeferredDirectiveOnQueryOperation.java | 41 ------- .../rules/DeferredMustBeOnAllFields.java | 100 ---------------- .../groovy/example/http/DeferHttpSupport.java | 2 +- .../execution/defer/DeferSupportTest.groovy | 6 +- .../execution/defer/DeferredCallTest.groovy | 2 +- .../LegacyTestingInstrumentation.groovy | 11 +- .../DataLoaderPerformanceData.groovy | 110 +++++++++++++++++- .../DeferredDirectiveAbstractRuleTest.groovy | 40 ------- ...rredDirectiveOnNonNullableFieldTest.groovy | 46 -------- ...ferredDirectiveOnQueryOperationTest.groovy | 71 ----------- src/test/groovy/readme/DeferredExamples.java | 2 +- 26 files changed, 234 insertions(+), 444 deletions(-) delete mode 100644 src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java delete mode 100644 src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java delete mode 100644 src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java delete mode 100644 src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy delete mode 100644 src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy delete mode 100644 src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy diff --git a/src/main/java/graphql/DeferredExecutionResultImpl.java b/src/main/java/graphql/DeferredExecutionResultImpl.java index c47a9a5312..4aad7e1f85 100644 --- a/src/main/java/graphql/DeferredExecutionResultImpl.java +++ b/src/main/java/graphql/DeferredExecutionResultImpl.java @@ -1,6 +1,6 @@ package graphql; -import graphql.execution.ExecutionPath; +import graphql.execution.ResultPath; import java.util.Collections; import java.util.LinkedHashMap; @@ -45,7 +45,7 @@ public static class Builder { private List path = Collections.emptyList(); private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); - public Builder path(ExecutionPath path) { + public Builder path(ResultPath path) { this.path = assertNotNull(path).toList(); return this; } @@ -61,8 +61,11 @@ public Builder addErrors(List errors) { } public DeferredExecutionResult build() { - ExecutionResultImpl build = builder.build(); - return new DeferredExecutionResultImpl(path, build); + // TODO: This class will be deleted anyway +// ExecutionResultImpl build = builder.build(); +// return new DeferredExecutionResultImpl(path, build); + + return null; } } } diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index 56f7a2f9be..f76130fc98 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -21,6 +21,16 @@ @SuppressWarnings("FutureReturnValueIgnored") public class Async { + public static void copyResults(CompletableFuture source, CompletableFuture target) { + source.whenComplete((o, throwable) -> { + if (throwable != null) { + target.completeExceptionally(throwable); + return; + } + target.complete(o); + }); + } + public interface CombinedBuilder { void add(CompletableFuture completableFuture); diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 8689fb54cf..6aedc12863 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -1,26 +1,25 @@ package graphql.execution; import graphql.ExecutionResult; +import graphql.PublicApi; import graphql.execution.defer.DeferSupport; import graphql.execution.defer.DeferredCall; import graphql.execution.defer.DeferredErrorSupport; import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; -import graphql.PublicApi; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLObjectType; +import graphql.util.FpKit; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; import java.util.function.Supplier; -import java.util.stream.Collectors; import static graphql.execution.MergedSelectionSet.newMergedSelectionSet; @@ -106,7 +105,7 @@ public CompletableFuture execute(ExecutionContext executionCont private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedField currentField) { DeferSupport deferSupport = executionContext.getDeferSupport(); - if (deferSupport.checkForDeferDirective(currentField, executionContext.getVariables())) { + if (deferSupport.checkForDeferDirective(currentField, executionContext)) { DeferredErrorSupport errorSupport = new DeferredErrorSupport(); // with a deferred field we are really resetting where we execute from, that is from this current field onwards @@ -119,9 +118,7 @@ private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyP builder.deferredErrorSupport(errorSupport) .field(currentField) .fields(mergedSelectionSet) - .parent(null) // this is a break in the parent -> child chain - its a new start effectively - .listSize(0) - .currentListIndex(0); + .parent(null); // this is a break in the parent -> child chain - its a new start effectively } ); @@ -136,18 +133,25 @@ private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyP private Supplier> deferredExecutionResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { return () -> { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); - GraphQLObjectType fieldContainer = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); + // TODO: freis: This is suddenly not needed anymore +// GraphQLObjectType fieldContainer = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); Instrumentation instrumentation = executionContext.getInstrumentation(); - DeferredFieldInstrumentationContext fieldCtx = instrumentation.beginDeferredField( - new InstrumentationDeferredFieldParameters(executionContext, parameters, fieldDef, createExecutionStepInfo(executionContext, parameters, fieldDef, fieldContainer)) + + Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); + + InstrumentationContext fieldCtx = instrumentation.beginDeferredField( + new InstrumentationDeferredFieldParameters(executionContext, executionStepInfo, parameters), + executionContext.getInstrumentationState() ); + CompletableFuture result = new CompletableFuture<>(); fieldCtx.onDispatched(result); CompletableFuture fieldValueInfoFuture = resolveFieldWithInfo(executionContext, parameters); fieldValueInfoFuture.whenComplete((fieldValueInfo, throwable) -> { - fieldCtx.onFieldValueInfo(fieldValueInfo); + // TODO: +// fieldCtx.onFieldValueInfo(fieldValueInfo); CompletableFuture execResultFuture = fieldValueInfo.getFieldValue(); execResultFuture = execResultFuture.whenComplete(fieldCtx::onCompleted); diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 28c8f7f6da..52b50899bb 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -6,10 +6,8 @@ import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.GraphQLError; -import graphql.Internal; import graphql.PublicApi; import graphql.collect.ImmutableKit; -import graphql.cachecontrol.CacheControl; import graphql.execution.defer.DeferSupport; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 3b35947c3b..4fe01aa112 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -221,6 +221,13 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex return result; } + protected CompletableFuture resolveFieldWithInfoToNull(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + FetchedValue fetchedValue = FetchedValue.newFetchedValue().build(); + FieldValueInfo fieldValueInfo = completeField(executionContext, parameters, fetchedValue); + return CompletableFuture.completedFuture(fieldValueInfo); + } + + /** * Called to fetch a value for a field from the {@link DataFetcher} associated with the field * {@link GraphQLFieldDefinition}. @@ -291,7 +298,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC .handle((result, exception) -> { fetchCtx.onCompleted(result, exception); if (exception != null) { - return handleFetchingException(dataFetchingEnvironment.get(), exception); + return handleFetchingException(dataFetchingEnvironment.get(), parameters, exception); } else { // we can simply return the fetched value CF and avoid a allocation return fetchedValue; @@ -366,13 +373,12 @@ private void addExtensionsIfPresent(ExecutionContext executionContext, DataFetch protected CompletableFuture handleFetchingException( DataFetchingEnvironment environment, - Throwable e) { + ExecutionStrategyParameters parameters, Throwable e) { DataFetcherExceptionHandlerParameters handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() .dataFetchingEnvironment(environment) .exception(e) .build(); - // TODO: parameters here is an instance of ExecutionStrategyParameters // TODO: not sure if this method call goes here, inside the try block below, or in the async method parameters.deferredErrorSupport().onFetchingException(parameters, e); diff --git a/src/main/java/graphql/execution/defer/DeferSupport.java b/src/main/java/graphql/execution/defer/DeferSupport.java index 7e6fb97429..55261911b3 100644 --- a/src/main/java/graphql/execution/defer/DeferSupport.java +++ b/src/main/java/graphql/execution/defer/DeferSupport.java @@ -1,9 +1,8 @@ package graphql.execution.defer; import graphql.DeferredExecutionResult; -import graphql.Directives; -import graphql.ExecutionResult; import graphql.Internal; +import graphql.execution.ExecutionContext; import graphql.execution.MergedField; import graphql.execution.ValuesResolver; import graphql.execution.reactive.SingleSubscriberPublisher; @@ -18,25 +17,33 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; -import static graphql.Directives.*; +import static graphql.Directives.DeferDirective; /** * This provides support for @defer directives on fields that mean that results will be sent AFTER * the main result is sent via a Publisher stream. */ @Internal +// TODO: This should be called IncrementalSupport and handle both @defer and @stream public class DeferSupport { private final AtomicBoolean deferDetected = new AtomicBoolean(false); private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); - private final ValuesResolver valuesResolver = new ValuesResolver(); - public boolean checkForDeferDirective(MergedField currentField, Map variables) { + public boolean checkForDeferDirective(MergedField currentField, ExecutionContext executionContext) { for (Field field : currentField.getFields()) { - Directive directive = field.getDirective(DeferDirective.getName()); + List directives = field.getDirectives(DeferDirective.getName()); + // TODO: How to best deal with repeated directives here - @defer/@stream is not a repeated directive + Directive directive = directives.stream().findFirst().orElse(null); if (directive != null) { - Map argumentValues = valuesResolver.getArgumentValues(DeferDirective.getArguments(), directive.getArguments(), variables); + Map argumentValues = ValuesResolver.getArgumentValues( + DeferDirective.getArguments(), + directive.getArguments(), + executionContext.getCoercedVariables(), + executionContext.getGraphQLContext(), + executionContext.getLocale() + ); return (Boolean) argumentValues.get("if"); } } diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index f1da0c8683..b52e124d5b 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -5,7 +5,7 @@ import graphql.ExecutionResult; import graphql.GraphQLError; import graphql.Internal; -import graphql.execution.ExecutionPath; +import graphql.execution.ResultPath; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -17,11 +17,11 @@ */ @Internal public class DeferredCall { - private final ExecutionPath path; + private final ResultPath path; private final Supplier> call; private final DeferredErrorSupport errorSupport; - public DeferredCall(ExecutionPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { + public DeferredCall(ResultPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { this.path = path; this.call = call; this.errorSupport = deferredErrorSupport; diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 697c4e6241..1a3089584b 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -177,13 +177,16 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument } @Override - public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { - return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() - .map(instrumentation -> { - InstrumentationState state = getState(instrumentation, parameters.getInstrumentationState()); - return instrumentation.beginDeferredField(parameters.withNewState(state)); - }) - .collect(toList())); + public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { +// return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() +// .map(instrumentation -> { +// InstrumentationState specificState = getSpecificState(instrumentation, parameters.getInstrumentationState()); +// return instrumentation.beginDeferredField(parameters, specificState); +// }) +// .collect(Collectors.toList())); + + // TODO: Fix this + throw new UnsupportedOperationException("TODO: fix this"); } @Override @@ -445,28 +448,28 @@ public void onFieldValuesException() { } } - private static class ChainedDeferredExecutionStrategyInstrumentationContext implements DeferredFieldInstrumentationContext { - - private final List contexts; - - ChainedDeferredExecutionStrategyInstrumentationContext(List contexts) { - this.contexts = Collections.unmodifiableList(contexts); - } - - @Override - public void onDispatched(CompletableFuture result) { - contexts.forEach(context -> context.onDispatched(result)); - } - - @Override - public void onCompleted(ExecutionResult result, Throwable t) { - contexts.forEach(context -> context.onCompleted(result, t)); - } - - @Override - public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - contexts.forEach(context -> context.onFieldValueInfo(fieldValueInfo)); - } - } +// private static class ChainedDeferredExecutionStrategyInstrumentationContext implements DeferredFieldInstrumentationContext { +// +// private final List contexts; +// +// ChainedDeferredExecutionStrategyInstrumentationContext(List> contexts) { +// this.contexts = Collections.unmodifiableList(contexts); +// } +// +// @Override +// public void onDispatched(CompletableFuture result) { +// contexts.forEach(context -> context.onDispatched(result)); +// } +// +// @Override +// public void onCompleted(ExecutionResult result, Throwable t) { +// contexts.forEach(context -> context.onCompleted(result, t)); +// } +// +// @Override +// public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { +// contexts.forEach(context -> context.onFieldValueInfo(fieldValueInfo)); +// } +// } } diff --git a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java index 04fbceab81..00228c6190 100644 --- a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java @@ -4,6 +4,7 @@ import graphql.Internal; import graphql.PublicSpi; import graphql.execution.FieldValueInfo; +import graphql.execution.MergedField; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -20,6 +21,10 @@ default void onFieldValuesException() { } + default void onDeferredField(MergedField field) { + + } + /** * This creates a no-op {@link InstrumentationContext} if the one pass in is null * diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 3b3b7277e2..aed0e707b7 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -230,7 +230,9 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen * * @return a non null {@link InstrumentationContext} object that will be called back when the step ends */ - DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters); + default InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { + return noOp(); + } /** * This is called each time a subscription field produces a new reactive stream event value and it needs to be mapped over via the graphql field subselection. diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 475e30d053..4ce01319ed 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -137,8 +137,8 @@ private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContex } @Override - public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { - DataLoaderDispatcherInstrumentationState state = parameters.getInstrumentationState(); + public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState rawState) { + DataLoaderDispatcherInstrumentationState state = ofState(rawState); // // if there are no data loaders, there is nothing to do // @@ -154,7 +154,7 @@ public void onCompleted(ExecutionResult result, Throwable t) { }; } - return state.getApproach().beginDeferredField(parameters.withNewState(state.getState())); + return state.getApproach().beginDeferredField(parameters, state.getState()); } @Override diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 357bbe2dbb..3dc207b7a4 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -189,8 +189,9 @@ private int getCountForList(List fieldValueInfos) { return result; } - DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { - CallStack callStack = parameters.getInstrumentationState(); + DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { + // TODO: Is this cast safe? + CallStack callStack = (CallStack) state; int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); synchronized (callStack) { callStack.clearAndMarkCurrentLevelAsReady(level); diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java index f8a0f47c2e..64714049b2 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java @@ -4,7 +4,8 @@ import graphql.execution.ExecutionStepInfo; import graphql.execution.ExecutionStrategyParameters; import graphql.execution.instrumentation.InstrumentationState; -import graphql.schema.GraphQLFieldDefinition; + +import java.util.function.Supplier; /** * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods @@ -13,28 +14,15 @@ public class InstrumentationDeferredFieldParameters extends InstrumentationField private final ExecutionStrategyParameters executionStrategyParameters; - public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, GraphQLFieldDefinition fieldDef, ExecutionStepInfo executionStepInfo) { - this(executionContext, executionStrategyParameters, fieldDef, executionStepInfo, executionContext.getInstrumentationState()); + public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, ExecutionStrategyParameters executionStrategyParameters) { + this(executionContext, executionStepInfo, executionContext.getInstrumentationState(), executionStrategyParameters); } - InstrumentationDeferredFieldParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, GraphQLFieldDefinition fieldDef, ExecutionStepInfo executionStepInfo, InstrumentationState instrumentationState) { - super(executionContext, fieldDef, executionStepInfo, instrumentationState); + InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, InstrumentationState instrumentationState, ExecutionStrategyParameters executionStrategyParameters) { + super(executionContext, executionStepInfo, instrumentationState); this.executionStrategyParameters = executionStrategyParameters; } - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - */ - @Override - public InstrumentationDeferredFieldParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationDeferredFieldParameters( - this.getExecutionContext(), this.executionStrategyParameters, this.getField(), this.getExecutionStepInfo(), instrumentationState); - } - public ExecutionStrategyParameters getExecutionStrategyParameters() { return executionStrategyParameters; } diff --git a/src/main/java/graphql/validation/Validator.java b/src/main/java/graphql/validation/Validator.java index 421fedc7c4..c799ff9609 100644 --- a/src/main/java/graphql/validation/Validator.java +++ b/src/main/java/graphql/validation/Validator.java @@ -6,10 +6,6 @@ import graphql.language.Document; import graphql.schema.GraphQLSchema; import graphql.validation.rules.ArgumentsOfCorrectType; -import graphql.validation.rules.UniqueObjectFieldName; -import graphql.validation.rules.DeferredDirectiveOnNonNullableField; -import graphql.validation.rules.DeferredDirectiveOnQueryOperation; -import graphql.validation.rules.DeferredMustBeOnAllFields; import graphql.validation.rules.ExecutableDefinitions; import graphql.validation.rules.FieldsOnCorrectType; import graphql.validation.rules.FragmentsOnCompositeType; @@ -25,19 +21,17 @@ import graphql.validation.rules.OverlappingFieldsCanBeMerged; import graphql.validation.rules.PossibleFragmentSpreads; import graphql.validation.rules.ProvidedNonNullArguments; -import graphql.validation.rules.ScalarLeafs; import graphql.validation.rules.ScalarLeaves; import graphql.validation.rules.SubscriptionUniqueRootField; import graphql.validation.rules.UniqueArgumentNames; import graphql.validation.rules.UniqueDirectiveNamesPerLocation; import graphql.validation.rules.UniqueFragmentNames; +import graphql.validation.rules.UniqueObjectFieldName; import graphql.validation.rules.UniqueOperationNames; import graphql.validation.rules.UniqueVariableNames; import graphql.validation.rules.VariableDefaultValuesOfCorrectType; import graphql.validation.rules.VariableTypesMatch; import graphql.validation.rules.VariablesAreInputTypes; -import graphql.validation.rules.UniqueArgumentNamesRule; -import graphql.validation.rules.UniqueVariableNamesRule; import java.util.ArrayList; import java.util.List; @@ -154,16 +148,6 @@ public List createRules(ValidationContext validationContext, Valid UniqueArgumentNames uniqueArgumentNamesRule = new UniqueArgumentNames(validationContext, validationErrorCollector); rules.add(uniqueArgumentNamesRule); - // our extensions beyond spec - DeferredDirectiveOnNonNullableField deferredDirectiveOnNonNullableField = new DeferredDirectiveOnNonNullableField(validationContext, validationErrorCollector); - rules.add(deferredDirectiveOnNonNullableField); - - DeferredDirectiveOnQueryOperation deferredDirectiveOnQueryOperation = new DeferredDirectiveOnQueryOperation(validationContext, validationErrorCollector); - rules.add(deferredDirectiveOnQueryOperation); - - DeferredMustBeOnAllFields deferredMustBeOnAllFields = new DeferredMustBeOnAllFields(validationContext, validationErrorCollector); - rules.add(deferredMustBeOnAllFields); - UniqueVariableNames uniqueVariableNamesRule = new UniqueVariableNames(validationContext, validationErrorCollector); rules.add(uniqueVariableNamesRule); diff --git a/src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java b/src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java deleted file mode 100644 index 8de40de2c5..0000000000 --- a/src/main/java/graphql/validation/rules/DeferredDirectiveOnNonNullableField.java +++ /dev/null @@ -1,32 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.Directive; -import graphql.language.Node; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLTypeUtil; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; -import graphql.validation.ValidationErrorType; - -import java.util.List; - -@Internal -public class DeferredDirectiveOnNonNullableField extends DeferredDirectiveAbstractRule { - - - public DeferredDirectiveOnNonNullableField(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - protected void onDeferredDirective(Directive deferredDirective, List ancestors, GraphQLCompositeType parentType, GraphQLFieldDefinition fieldDef) { - GraphQLOutputType fieldDefType = fieldDef.getType(); - if (!GraphQLTypeUtil.isNullable(fieldDefType)) { - String message = String.format("@defer directives can only be applied to nullable fields - %s.%s is non nullable", parentType.getName(), fieldDef.getName()); - addError(ValidationErrorType.DeferDirectiveOnNonNullField, deferredDirective.getSourceLocation(), message); - } - } -} diff --git a/src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java b/src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java deleted file mode 100644 index 55d7ed0c9a..0000000000 --- a/src/main/java/graphql/validation/rules/DeferredDirectiveOnQueryOperation.java +++ /dev/null @@ -1,41 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.Directive; -import graphql.language.Node; -import graphql.language.OperationDefinition; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLFieldDefinition; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; -import graphql.validation.ValidationErrorType; - -import java.util.List; -import java.util.Optional; - -@Internal -public class DeferredDirectiveOnQueryOperation extends DeferredDirectiveAbstractRule { - - public DeferredDirectiveOnQueryOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - protected void onDeferredDirective(Directive deferredDirective, List ancestors, GraphQLCompositeType parentType, GraphQLFieldDefinition fieldDef) { - Optional operationDefinition = getOperation(ancestors); - if (operationDefinition.isPresent()) { - OperationDefinition.Operation operation = operationDefinition.get().getOperation(); - if (operation != OperationDefinition.Operation.QUERY) { - String message = String.format("@defer directives can only be applied to QUERY operations and not '%s' operations - %s.%s ", operation, parentType.getName(), fieldDef.getName()); - addError(ValidationErrorType.DeferDirectiveNotOnQueryOperation, deferredDirective.getSourceLocation(), message); - } - } - } - - private Optional getOperation(List ancestors) { - return ancestors.stream() - .filter(def -> def instanceof OperationDefinition) - .map((def -> (OperationDefinition) def)) - .findFirst(); - } -} diff --git a/src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java b/src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java deleted file mode 100644 index 3f7b019ec5..0000000000 --- a/src/main/java/graphql/validation/rules/DeferredMustBeOnAllFields.java +++ /dev/null @@ -1,100 +0,0 @@ -package graphql.validation.rules; - -import graphql.Directives; -import graphql.Internal; -import graphql.language.Document; -import graphql.language.Field; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.InlineFragment; -import graphql.language.Selection; -import graphql.language.SelectionSet; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationError; -import graphql.validation.ValidationErrorCollector; -import graphql.validation.ValidationErrorType; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static java.util.stream.Collectors.groupingBy; - -@Internal -public class DeferredMustBeOnAllFields extends AbstractRule { - - private final Map, List> fieldsByPath = new LinkedHashMap<>(); - - public DeferredMustBeOnAllFields(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void documentFinished(Document document) { - fieldsByPath.forEach((path, allFieldsOnPath) -> { - Map> fieldsByName = allFieldsOnPath.stream() - .collect(groupingBy(Field::getName)); - fieldsByName.forEach((fieldName, fields) -> { - if (fields.size() > 1) { - if (!allFieldsHaveDeferredDirectiveIfAnyOneDoes(fields)) { - recordBadDeferredFields(path, fieldName, fields); - } - } - }); - }); - } - - private boolean allFieldsHaveDeferredDirectiveIfAnyOneDoes(List fields) { - long count = fields.stream() - .filter(fld -> fld.getDirective(Directives.DeferDirective.getName()) != null) - .count(); - if (count == 0) { - return true; - } else { - return count == fields.size(); - } - } - - private void recordBadDeferredFields(List path, String fieldName, List fields) { - String message = String.format("If any of the multiple declarations of a field within the query (via fragments and field selections) contain the @defer directive, then all of them have to contain the @defer directive - field '%s' at '%s'", fieldName, path); - getValidationErrorCollector().addError(new ValidationError(ValidationErrorType.DeferMustBeOnAllFields, fields.get(0).getSourceLocation(), message, path)); - } - - @Override - public void checkSelectionSet(SelectionSet selectionSet) { - List queryPath = getValidationContext().getQueryPath(); - ArrayList> seenSelections = new ArrayList<>(); - addFields(queryPath, selectionSet, seenSelections); - } - - private void addFields(List path, SelectionSet selectionSet, List> seenSelections) { - if (path == null) { - path = Collections.emptyList(); - } - for (Selection selection : selectionSet.getSelections()) { - // sett issue 1817 - an illegal circular query could cause a stack overflow - protected against it - if (seenSelections.contains(selection)) { - break; - } - seenSelections.add(selection); - if (selection instanceof Field) { - List fields = fieldsByPath.getOrDefault(path, new ArrayList<>()); - fields.add((Field) selection); - fieldsByPath.put(path, fields); - } - if (selection instanceof InlineFragment) { - addFields(path, ((InlineFragment) selection).getSelectionSet(), seenSelections); - } - if (selection instanceof FragmentSpread) { - FragmentSpread fragmentSpread = (FragmentSpread) selection; - FragmentDefinition fragmentDefinition = getValidationContext().getFragment(fragmentSpread.getName()); - if (fragmentDefinition != null) { - addFields(path, fragmentDefinition.getSelectionSet(), seenSelections); - } - } - } - } -} diff --git a/src/test/groovy/example/http/DeferHttpSupport.java b/src/test/groovy/example/http/DeferHttpSupport.java index 23f2c3dedf..a0ef11f625 100644 --- a/src/test/groovy/example/http/DeferHttpSupport.java +++ b/src/test/groovy/example/http/DeferHttpSupport.java @@ -3,11 +3,11 @@ import graphql.DeferredExecutionResult; import graphql.ExecutionResult; import graphql.GraphQL; +import jakarta.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.io.UncheckedIOException; diff --git a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy index 7f2c41e364..7f592c6db4 100644 --- a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy +++ b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy @@ -3,7 +3,7 @@ package graphql.execution.defer import graphql.DeferredExecutionResult import graphql.ExecutionResult import graphql.ExecutionResultImpl -import graphql.execution.ExecutionPath +import graphql.execution.ResultPath import graphql.language.Argument import graphql.language.BooleanValue import graphql.language.Directive @@ -255,7 +255,7 @@ class DeferSupportTest extends Specification { new ExecutionResultImpl(data, []) }) } - return new DeferredCall(ExecutionPath.parse(path), callSupplier, new DeferredErrorSupport()) + return new DeferredCall(ResultPath.parse(path), callSupplier, new DeferredErrorSupport()) } private @@ -267,6 +267,6 @@ class DeferSupportTest extends Specification { new ExecutionResultImpl(dataParent, []) }) } - return new DeferredCall(ExecutionPath.parse("/field/path"), callSupplier, new DeferredErrorSupport()) + return new DeferredCall(ResultPath.parse("/field/path"), callSupplier, new DeferredErrorSupport()) } } diff --git a/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy b/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy index 2e703bc875..fb6ffa6d57 100644 --- a/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy +++ b/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy @@ -6,7 +6,7 @@ import graphql.validation.ValidationError import graphql.validation.ValidationErrorType import spock.lang.Specification -import static graphql.execution.ExecutionPath.parse +import static graphql.execution.ResultPath.parse import static java.util.concurrent.CompletableFuture.completedFuture class DeferredCallTest extends Specification { diff --git a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy index 0417d7b797..8bd57196a1 100644 --- a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy @@ -68,11 +68,12 @@ class LegacyTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("execute-operation", executionList, throwableList, useOnDispatch) } - @Override - DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState - return new TestingInstrumentContext("deferred-field-$parameters.field.name", executionList, throwableList) - } + // TODO: Need that? +// @Override +// DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { +// assert parameters.getInstrumentationState() == instrumentationState +// return new TestingInstrumentContext("deferred-field-$parameters.field.name", executionList, throwableList) +// } @Override InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy index 7ca398f084..a0fec6c9da 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy @@ -1,11 +1,12 @@ package graphql.execution.instrumentation.dataloader - +import graphql.Directives import graphql.GraphQL import graphql.execution.instrumentation.Instrumentation import graphql.schema.GraphQLSchema import org.dataloader.DataLoaderRegistry + class DataLoaderPerformanceData { private final BatchCompareDataFetchers batchCompareDataFetchers; @@ -22,6 +23,7 @@ class DataLoaderPerformanceData { GraphQL setupGraphQL(Instrumentation instrumentation) { GraphQLSchema schema = new BatchCompare().buildDataLoaderSchema(batchCompareDataFetchers) + schema = schema.transform({ bldr -> bldr.additionalDirective(Directives.DeferDirective) }) GraphQL.newGraphQL(schema) .instrumentation(instrumentation) @@ -154,4 +156,110 @@ class DataLoaderPerformanceData { } } """ + + static def expectedInitialDeferredData = [ + shops: [ + [id: "shop-1", name: "Shop 1", departments: null], + [id: "shop-2", name: "Shop 2", departments: null], + [id: "shop-3", name: "Shop 3", departments: null], + ] + ] + + static def expectedInitialExpensiveDeferredData = [ + shops : [ + [id: "shop-1", name: "Shop 1", departments: null, expensiveDepartments: null], + [id: "shop-2", name: "Shop 2", departments: null, expensiveDepartments: null], + [id: "shop-3", name: "Shop 3", departments: null, expensiveDepartments: null], + ], + expensiveShops: null + ] + + static def expectedListOfDeferredData = [ + [[id: "department-1", name: "Department 1", products: [[id: "product-1", name: "Product 1"]]], + [id: "department-2", name: "Department 2", products: [[id: "product-2", name: "Product 2"]]], + [id: "department-3", name: "Department 3", products: [[id: "product-3", name: "Product 3"]]]] + , + + [[id: "department-4", name: "Department 4", products: [[id: "product-4", name: "Product 4"]]], + [id: "department-5", name: "Department 5", products: [[id: "product-5", name: "Product 5"]]], + [id: "department-6", name: "Department 6", products: [[id: "product-6", name: "Product 6"]]]] + , + [[id: "department-7", name: "Department 7", products: [[id: "product-7", name: "Product 7"]]], + [id: "department-8", name: "Department 8", products: [[id: "product-8", name: "Product 8"]]], + [id: "department-9", name: "Department 9", products: [[id: "product-9", name: "Product 9"]]]] + , + + ] + + + static def deferredQuery = """ + query { + shops { + id name + departments @defer { + id name + products { + id name + } + } + } + } + """ + + static def expensiveDeferredQuery = """ + query { + shops { + id name + departments @defer { + name + products @defer { + name + } + expensiveProducts @defer { + name + } + } + expensiveDepartments @defer { + name + products { + name + } + expensiveProducts { + name + } + } + } + expensiveShops @defer { + id name + } + } + """ + + static def expectedExpensiveDeferredData = [ + [[id: "exshop-1", name: "ExShop 1"], [id: "exshop-2", name: "ExShop 2"], [id: "exshop-3", name: "ExShop 3"]], + [[name: "Department 1",products:null, expensiveProducts:null], [name: "Department 2",products:null, expensiveProducts:null], [name: "Department 3",products:null, expensiveProducts:null]], + [[name: "Department 1", products: [[name: "Product 1"]], expensiveProducts: [[name: "Product 1"]]], [name: "Department 2", products: [[name: "Product 2"]], expensiveProducts: [[name: "Product 2"]]], [name: "Department 3", products: [[name: "Product 3"]], expensiveProducts: [[name: "Product 3"]]]], + [[name: "Department 4",products:null, expensiveProducts:null], [name: "Department 5",products:null, expensiveProducts:null], [name: "Department 6",products:null, expensiveProducts:null]], + [[name: "Department 4", products: [[name: "Product 4"]], expensiveProducts: [[name: "Product 4"]]], [name: "Department 5", products: [[name: "Product 5"]], expensiveProducts: [[name: "Product 5"]]], [name: "Department 6", products: [[name: "Product 6"]], expensiveProducts: [[name: "Product 6"]]]], + [[name: "Department 7",products:null, expensiveProducts:null], [name: "Department 8",products:null, expensiveProducts:null], [name: "Department 9",products:null, expensiveProducts:null]], + [[name: "Department 7", products: [[name: "Product 7"]], expensiveProducts: [[name: "Product 7"]]], [name: "Department 8", products: [[name: "Product 8"]], expensiveProducts: [[name: "Product 8"]]], [name: "Department 9", products: [[name: "Product 9"]], expensiveProducts: [[name: "Product 9"]]]], + [[name: "Product 1"]], + [[name: "Product 1"]], + [[name: "Product 2"]], + [[name: "Product 2"]], + [[name: "Product 3"]], + [[name: "Product 3"]], + [[name: "Product 4"]], + [[name: "Product 4"]], + [[name: "Product 5"]], + [[name: "Product 5"]], + [[name: "Product 6"]], + [[name: "Product 6"]], + [[name: "Product 7"]], + [[name: "Product 7"]], + [[name: "Product 8"]], + [[name: "Product 8"]], + [[name: "Product 9"]], + [[name: "Product 9"]], + ] } diff --git a/src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy deleted file mode 100644 index 1e1947d443..0000000000 --- a/src/test/groovy/graphql/validation/rules/DeferredDirectiveAbstractRuleTest.groovy +++ /dev/null @@ -1,40 +0,0 @@ -package graphql.validation.rules - -import graphql.Directives -import graphql.language.Directive -import graphql.validation.ValidationContext -import graphql.validation.ValidationErrorCollector -import spock.lang.Specification - -class DeferredDirectiveAbstractRuleTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - DeferredDirectiveOnNonNullableField deferredRule = new DeferredDirectiveOnNonNullableField(validationContext, errorCollector) - - - def 'if directive name is not deferred then empty errors'() { - when: - deferredRule.checkDirective(new Directive("someOtherName"), []) - then: - errorCollector.errors.isEmpty() - } - - def 'if parent type is empty then empty errors'() { - validationContext.getParentType() >> null - - when: - deferredRule.checkDirective(new Directive(Directives.DeferDirective.name), []) - then: - errorCollector.errors.isEmpty() - } - - def 'if field def is empty then empty errors'() { - validationContext.getFieldDef() >> null - - when: - deferredRule.checkDirective(new Directive(Directives.DeferDirective.name), []) - then: - errorCollector.errors.isEmpty() - } - -} diff --git a/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy deleted file mode 100644 index fa3f69f46e..0000000000 --- a/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnNonNullableFieldTest.groovy +++ /dev/null @@ -1,46 +0,0 @@ -package graphql.validation.rules - -import graphql.Directives -import graphql.StarWarsSchema -import graphql.language.Directive -import graphql.validation.ValidationContext -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector -import graphql.validation.ValidationErrorType -import spock.lang.Specification - -import static graphql.Scalars.GraphQLString -import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition -import static graphql.schema.GraphQLNonNull.nonNull - -class DeferredDirectiveOnNonNullableFieldTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - DeferredDirectiveOnNonNullableField deferredOnNullableField = new DeferredDirectiveOnNonNullableField(validationContext, errorCollector) - - - def "denies non nullable fields"() { - def fieldDefinition = newFieldDefinition().name("field").type(nonNull(GraphQLString)).build() - - validationContext.getParentType() >> StarWarsSchema.humanType - validationContext.getFieldDef() >> fieldDefinition - - when: - deferredOnNullableField.checkDirective(new Directive(Directives.DeferDirective.name), []) - then: - !errorCollector.errors.isEmpty() - (errorCollector.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveOnNonNullField - } - - def "allows nullable fields"() { - def fieldDefinition = newFieldDefinition().name("field").type(GraphQLString).build() - - validationContext.getParentType() >> StarWarsSchema.humanType - validationContext.getFieldDef() >> fieldDefinition - - when: - deferredOnNullableField.checkDirective(new Directive(Directives.DeferDirective.name), []) - then: - errorCollector.errors.isEmpty() - } -} diff --git a/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy deleted file mode 100644 index 721fb2c89b..0000000000 --- a/src/test/groovy/graphql/validation/rules/DeferredDirectiveOnQueryOperationTest.groovy +++ /dev/null @@ -1,71 +0,0 @@ -package graphql.validation.rules - -import graphql.StarWarsSchema -import graphql.language.Document -import graphql.parser.Parser -import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor -import graphql.validation.TraversalContext -import graphql.validation.ValidationContext -import graphql.validation.ValidationErrorCollector -import graphql.validation.ValidationErrorType -import spock.lang.Specification - -import static graphql.Scalars.GraphQLString -import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition -import static graphql.schema.GraphQLNonNull.nonNull - -class DeferredDirectiveOnQueryOperationTest extends Specification { - - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - DeferredDirectiveOnQueryOperation directiveOnQueryOperation = new DeferredDirectiveOnQueryOperation(validationContext, errorCollector) - - void setup() { - def traversalContext = Mock(TraversalContext) - validationContext.getSchema() >> StarWarsSchema.starWarsSchema - validationContext.getTraversalContext() >> traversalContext - - def fieldDefinition = newFieldDefinition().name("field").type(nonNull(GraphQLString)).build() - - validationContext.getParentType() >> StarWarsSchema.humanType - validationContext.getFieldDef() >> fieldDefinition - } - - def "denies operations that are not queries"() { - given: - def query = """ - mutation Foo { - name @defer - } - """ - - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [directiveOnQueryOperation])) - - then: - errorCollector.containsValidationError(ValidationErrorType.DeferDirectiveNotOnQueryOperation) - } - - def "allows operations that are queries"() { - given: - def query = """ - query Foo { - name @defer - } - """ - - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [directiveOnQueryOperation])) - - then: - errorCollector.errors.isEmpty() - } - -} diff --git a/src/test/groovy/readme/DeferredExamples.java b/src/test/groovy/readme/DeferredExamples.java index 23aff5d472..3d8d7e455e 100644 --- a/src/test/groovy/readme/DeferredExamples.java +++ b/src/test/groovy/readme/DeferredExamples.java @@ -6,11 +6,11 @@ import graphql.ExecutionResult; import graphql.GraphQL; import graphql.schema.GraphQLSchema; +import jakarta.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import javax.servlet.http.HttpServletResponse; import java.util.Map; @SuppressWarnings({"unused", "ConstantConditions", "UnusedAssignment", "unchecked"}) From bd2c030916c24db511b58cdbeff9510f9b1bd63a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:45:27 +0000 Subject: [PATCH 100/393] Bump google-github-actions/auth from 2.0.0 to 2.0.1 Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v2.0.0...v2.0.1) --- updated-dependencies: - dependency-name: google-github-actions/auth dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/invoke_test_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index 1b449a6a4f..e23e1c5768 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -50,7 +50,7 @@ jobs: - id: 'auth' name: 'Authenticate to Google Cloud' - uses: google-github-actions/auth@v2.0.0 + uses: google-github-actions/auth@v2.0.1 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} From e86286e2fd0839abb2ce3f7d174bfae18fbaf357 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 18 Jan 2024 16:16:46 +1100 Subject: [PATCH 101/393] WIP: hacky defer is working, some new tests for basic stuff, but a lot still missing --- src/main/java/graphql/ExecutionInput.java | 29 +- .../execution/AsyncExecutionStrategy.java | 149 ++++- .../java/graphql/execution/Execution.java | 24 +- .../graphql/execution/ExecutionContext.java | 6 +- .../graphql/execution/ExecutionStrategy.java | 6 +- .../graphql/execution/FieldCollector.java | 54 +- .../java/graphql/execution/MergedField.java | 34 +- .../graphql/execution/MergedSelectionSet.java | 2 +- .../defer/DeferExecutionSupport.java | 128 ++++ .../graphql/execution/defer/DeferSupport.java | 89 --- .../graphql/execution/defer/DeferredCall.java | 59 +- .../execution/defer/DeferredSelectionSet.java | 22 + .../execution/incremental/DeferExecution.java | 19 + .../incremental/IncrementalUtils.java | 66 ++ .../graphql/incremental/DeferPayload.java | 9 + .../DelayedIncrementalExecutionResult.java | 6 + ...DelayedIncrementalExecutionResultImpl.java | 20 + .../IncrementalExecutionResultImpl.java | 5 + .../incremental/IncrementalPayload.java | 2 + .../normalized/ExecutableNormalizedField.java | 18 +- .../ExecutableNormalizedOperationFactory.java | 39 +- ...tableNormalizedOperationToAstCompiler.java | 8 +- .../incremental/IncrementalNodes.java | 9 +- ...ion.java => NormalizedDeferExecution.java} | 4 +- .../defer/CapturingSubscriber.groovy | 17 +- .../execution/defer/DeferSupportTest.groovy | 18 +- .../DeferSupportIntegrationTest.groovy | 595 ++++++++++++++++++ 27 files changed, 1238 insertions(+), 199 deletions(-) create mode 100644 src/main/java/graphql/execution/defer/DeferExecutionSupport.java delete mode 100644 src/main/java/graphql/execution/defer/DeferSupport.java create mode 100644 src/main/java/graphql/execution/defer/DeferredSelectionSet.java create mode 100644 src/main/java/graphql/execution/incremental/DeferExecution.java create mode 100644 src/main/java/graphql/execution/incremental/IncrementalUtils.java rename src/main/java/graphql/normalized/incremental/{DeferExecution.java => NormalizedDeferExecution.java} (96%) create mode 100644 src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index bfda46a3d3..46bec9ec3a 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -29,6 +29,7 @@ public class ExecutionInput { private final DataLoaderRegistry dataLoaderRegistry; private final ExecutionId executionId; private final Locale locale; + private final boolean incrementalSupport; @Internal @@ -44,6 +45,7 @@ private ExecutionInput(Builder builder) { this.locale = builder.locale != null ? builder.locale : Locale.getDefault(); // always have a locale in place this.localContext = builder.localContext; this.extensions = builder.extensions; + this.incrementalSupport = builder.incrementalSupport; } /** @@ -139,6 +141,16 @@ public Map getExtensions() { return extensions; } + /** + * TODO: Javadoc + * @return + */ + @ExperimentalApi + public boolean isIncrementalSupport() { + return incrementalSupport; + } + + /** * 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. @@ -159,7 +171,8 @@ public ExecutionInput transform(Consumer builderConsumer) { .variables(this.rawVariables.toMap()) .extensions(this.extensions) .executionId(this.executionId) - .locale(this.locale); + .locale(this.locale) + .incrementalSupport(this.incrementalSupport); builderConsumer.accept(builder); @@ -216,6 +229,7 @@ public static class Builder { private DataLoaderRegistry dataLoaderRegistry = DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY; private Locale locale = Locale.getDefault(); private ExecutionId executionId; + public boolean incrementalSupport = false; public Builder query(String query) { this.query = assertNotNull(query, () -> "query can't be null"); @@ -379,8 +393,19 @@ public Builder dataLoaderRegistry(DataLoaderRegistry dataLoaderRegistry) { return this; } + /** + * TODO: Javadoc + * @param incrementalSupport + * @return + */ + @ExperimentalApi + public Builder incrementalSupport(boolean incrementalSupport) { + this.incrementalSupport = incrementalSupport; + return this; + } + public ExecutionInput build() { return new ExecutionInput(this); } } -} \ No newline at end of file +} diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 6aedc12863..1d1f53c1fe 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -1,11 +1,13 @@ package graphql.execution; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; import graphql.ExecutionResult; import graphql.PublicApi; -import graphql.execution.defer.DeferSupport; +import graphql.execution.defer.DeferExecutionSupport; import graphql.execution.defer.DeferredCall; import graphql.execution.defer.DeferredErrorSupport; -import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; +import graphql.execution.incremental.DeferExecution; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; @@ -13,13 +15,17 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.schema.GraphQLFieldDefinition; import graphql.util.FpKit; +import graphql.util.Pair; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.Supplier; +import java.util.stream.Collectors; import static graphql.execution.MergedSelectionSet.newMergedSelectionSet; @@ -56,7 +62,20 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); - Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size()); + +// if(true /* check if incremental support is enabled*/) { + SomethingDefer somethingDefer = new SomethingDefer( + fields, + parameters, + executionContext, + this::resolveFieldWithInfo + ); + + executionContext.getDeferSupport().enqueue(somethingDefer.createCalls()); +// } + + Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size() - somethingDefer.deferredFields.size()); + for (String fieldName : fieldNames) { MergedField currentField = fields.getSubField(fieldName); @@ -66,13 +85,14 @@ public CompletableFuture execute(ExecutionContext executionCont CompletableFuture future; - if (isDeferred(executionContext, newParameters, currentField)) { + if (somethingDefer.isDeferredField(currentField)) { executionStrategyCtx.onDeferredField(currentField); - future = resolveFieldWithInfoToNull(executionContext, newParameters); +// future = resolveFieldWithInfoToNull(executionContext, newParameters); } else { future = resolveFieldWithInfo(executionContext, newParameters); + futures.add(future); } - futures.add(future); + } CompletableFuture overallResult = new CompletableFuture<>(); executionStrategyCtx.onDispatched(overallResult); @@ -104,8 +124,9 @@ public CompletableFuture execute(ExecutionContext executionCont } private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedField currentField) { - DeferSupport deferSupport = executionContext.getDeferSupport(); - if (deferSupport.checkForDeferDirective(currentField, executionContext)) { + DeferExecutionSupport deferSupport = executionContext.getDeferSupport(); + + if (currentField.getDeferExecutions() != null && !currentField.getDeferExecutions().isEmpty()) { DeferredErrorSupport errorSupport = new DeferredErrorSupport(); // with a deferred field we are really resetting where we execute from, that is from this current field onwards @@ -122,8 +143,8 @@ private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyP } ); - DeferredCall call = new DeferredCall(parameters.getPath(), deferredExecutionResult(executionContext, callParameters), errorSupport); - deferSupport.enqueue(call); +// DeferredCall call = new DeferredCall(null /* TODO extract label somehow*/, parameters.getPath(), deferredExecutionResult(executionContext, callParameters), errorSupport); +// deferSupport.enqueue(call); return true; } return false; @@ -160,4 +181,112 @@ private Supplier> deferredExecutionResult(Exe return result; }; } + + + private static class SomethingDefer { + private final ImmutableListMultimap deferExecutionToFields; + private final ImmutableSet deferredFields; + private final ExecutionStrategyParameters parameters; + private final ExecutionContext executionContext; + private final BiFunction> resolveFieldWithInfoFn; + + private SomethingDefer( + MergedSelectionSet mergedSelectionSet, + ExecutionStrategyParameters parameters, + ExecutionContext executionContext, BiFunction> resolveFieldWithInfoFn + ) { + this.executionContext = executionContext; + this.resolveFieldWithInfoFn = resolveFieldWithInfoFn; + ImmutableListMultimap.Builder deferExecutionToFieldsBuilder = ImmutableListMultimap.builder(); + ImmutableSet.Builder deferredFieldsBuilder = ImmutableSet.builder(); + + mergedSelectionSet.getSubFields().values().forEach(mergedField -> { + mergedField.getDeferExecutions().forEach(de -> { + deferExecutionToFieldsBuilder.put(de, mergedField); + deferredFieldsBuilder.add(mergedField); + }); + }); + + this.deferExecutionToFields = deferExecutionToFieldsBuilder.build(); + this.deferredFields = deferredFieldsBuilder.build(); + this.parameters = parameters; + } + + private boolean isDeferredField(MergedField mergedField) { + return deferredFields.contains(mergedField); + } + + private Set createCalls() { + return deferExecutionToFields.keySet().stream().map(deferExecution -> { + DeferredErrorSupport errorSupport = new DeferredErrorSupport(); + + List mergedFields = deferExecutionToFields.get(deferExecution); + + List>> collect = mergedFields.stream() + .map(currentField -> { + Map fields = new LinkedHashMap<>(); + fields.put(currentField.getName(), currentField); + + ExecutionStrategyParameters callParameters = parameters.transform(builder -> + { + MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); + builder.deferredErrorSupport(errorSupport) + .field(currentField) + .fields(mergedSelectionSet) + .parent(null); // this is a break in the parent -> child chain - its a new start effectively + } + ); + + + return (Supplier>) () -> resolveFieldWithInfoFn + .apply(executionContext, callParameters) + .thenCompose(FieldValueInfo::getFieldValue) + .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult)); + + }) + .collect(Collectors.toList()); + + // with a deferred field we are really resetting where we execute from, that is from this current field onwards + return new DeferredCall( + deferExecution.getLabel(), + this.parameters.getPath(), + collect, + errorSupport + ); + }) + .collect(Collectors.toSet()); + } + + @SuppressWarnings("FutureReturnValueIgnored") + private Supplier> deferredExecutionResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + return () -> { +// GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); + // TODO: freis: This is suddenly not needed anymore +// GraphQLObjectType fieldContainer = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); + +// Instrumentation instrumentation = executionContext.getInstrumentation(); + +// Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); + +// InstrumentationContext fieldCtx = instrumentation.beginDeferredField( +// new InstrumentationDeferredFieldParameters(executionContext, executionStepInfo, parameters), +// executionContext.getInstrumentationState() +// ); + + CompletableFuture result = new CompletableFuture<>(); +// fieldCtx.onDispatched(result); + CompletableFuture fieldValueInfoFuture = resolveFieldWithInfoFn.apply(executionContext, parameters); + + fieldValueInfoFuture.whenComplete((fieldValueInfo, throwable) -> { + // TODO: +// fieldCtx.onFieldValueInfo(fieldValueInfo); + + CompletableFuture execResultFuture = fieldValueInfo.getFieldValue(); +// execResultFuture = execResultFuture.whenComplete(fieldCtx::onCompleted); + Async.copyResults(execResultFuture, result); + }); + return result; + }; + } + } } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 304a8e2e3b..3871e5e8ae 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -1,21 +1,21 @@ package graphql.execution; -import graphql.DeferredExecutionResult; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; -import graphql.GraphQL; import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; -import graphql.execution.defer.DeferSupport; +import graphql.execution.defer.DeferExecutionSupport; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.extensions.ExtensionsBuilder; +import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.IncrementalExecutionResultImpl; import graphql.language.Document; import graphql.language.FragmentDefinition; import graphql.language.NodeUtil; @@ -186,18 +186,26 @@ private CompletableFuture executeOperation(ExecutionContext exe } /* - * Adds the deferred publisher if its needed at the end of the query. This is also a good time for the deferred code to start running + * Adds the deferred publisher if it's needed at the end of the query. This is also a good time for the deferred code to start running */ private CompletableFuture deferSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { - DeferSupport deferSupport = executionContext.getDeferSupport(); + DeferExecutionSupport deferSupport = executionContext.getDeferSupport(); if (deferSupport.isDeferDetected()) { // we start the rest of the query now to maximize throughput. We have the initial important results // and now we can start the rest of the calls as early as possible (even before some one subscribes) - Publisher publisher = deferSupport.startDeferredCalls(); - return ExecutionResultImpl.newExecutionResult().from(er) - .addExtension(GraphQL.DEFERRED_RESULTS, publisher) + Publisher publisher = deferSupport.startDeferredCalls(); + + return IncrementalExecutionResultImpl.fromExecutionResult(er) + // TODO: would `hasNext` ever be false? + .hasNext(true) + .incrementalItemPublisher(publisher) .build(); +// +// +// return ExecutionResultImpl.newExecutionResult().from(er) +// .addExtension(GraphQL.DEFERRED_RESULTS, publisher) +// .build(); } return er; }); diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 52b50899bb..b9ca551e51 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -8,7 +8,7 @@ import graphql.GraphQLError; import graphql.PublicApi; import graphql.collect.ImmutableKit; -import graphql.execution.defer.DeferSupport; +import graphql.execution.defer.DeferExecutionSupport; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; @@ -54,7 +54,7 @@ public class ExecutionContext { private final Set errorPaths = new HashSet<>(); private final DataLoaderRegistry dataLoaderRegistry; private final Locale locale; - private final DeferSupport deferSupport = new DeferSupport(); + private final DeferExecutionSupport deferSupport = new DeferExecutionSupport(); private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; @@ -257,7 +257,7 @@ public ExecutionStrategy getSubscriptionStrategy() { return subscriptionStrategy; } - public DeferSupport getDeferSupport() { + public DeferExecutionSupport getDeferSupport() { return deferSupport; } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 4fe01aa112..6aaf13f097 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -696,7 +696,11 @@ protected CompletableFuture completeValueForObject(ExecutionCon .variables(executionContext.getCoercedVariables().toMap()) .build(); - MergedSelectionSet subFields = fieldCollector.collectFields(collectorParameters, parameters.getField()); + MergedSelectionSet subFields = fieldCollector.collectFields( + collectorParameters, + parameters.getField(), + executionContext.getExecutionInput().isIncrementalSupport() + ); ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull(resolvedObjectType); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, newExecutionStepInfo); diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index 8fae8a3afb..fec5619b1a 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -3,6 +3,8 @@ import graphql.Internal; import graphql.execution.conditional.ConditionalNodes; +import graphql.execution.incremental.DeferExecution; +import graphql.execution.incremental.IncrementalUtils; import graphql.language.Field; import graphql.language.FragmentDefinition; import graphql.language.FragmentSpread; @@ -33,13 +35,17 @@ public class FieldCollector { private final ConditionalNodes conditionalNodes = new ConditionalNodes(); public MergedSelectionSet collectFields(FieldCollectorParameters parameters, MergedField mergedField) { + return collectFields(parameters, mergedField, false); + } + + public MergedSelectionSet collectFields(FieldCollectorParameters parameters, MergedField mergedField, boolean incrementalSupport) { Map subFields = new LinkedHashMap<>(); Set visitedFragments = new LinkedHashSet<>(); for (Field field : mergedField.getFields()) { if (field.getSelectionSet() == null) { continue; } - this.collectFields(parameters, field.getSelectionSet(), visitedFragments, subFields); + this.collectFields(parameters, field.getSelectionSet(), visitedFragments, subFields, null, incrementalSupport); } return newMergedSelectionSet().subFields(subFields).build(); } @@ -53,27 +59,31 @@ public MergedSelectionSet collectFields(FieldCollectorParameters parameters, Mer * @return a map of the sub field selections */ public MergedSelectionSet collectFields(FieldCollectorParameters parameters, SelectionSet selectionSet) { + return collectFields(parameters, selectionSet, false); + } + + public MergedSelectionSet collectFields(FieldCollectorParameters parameters, SelectionSet selectionSet, boolean incrementalSupport) { Map subFields = new LinkedHashMap<>(); Set visitedFragments = new LinkedHashSet<>(); - this.collectFields(parameters, selectionSet, visitedFragments, subFields); + this.collectFields(parameters, selectionSet, visitedFragments, subFields, null, incrementalSupport); return newMergedSelectionSet().subFields(subFields).build(); } - private void collectFields(FieldCollectorParameters parameters, SelectionSet selectionSet, Set visitedFragments, Map fields) { + private void collectFields(FieldCollectorParameters parameters, SelectionSet selectionSet, Set visitedFragments, Map fields, DeferExecution deferExecution, boolean incrementalSupport) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { - collectField(parameters, fields, (Field) selection); + collectField(parameters, fields, (Field) selection, deferExecution); } else if (selection instanceof InlineFragment) { - collectInlineFragment(parameters, visitedFragments, fields, (InlineFragment) selection); + collectInlineFragment(parameters, visitedFragments, fields, (InlineFragment) selection, incrementalSupport); } else if (selection instanceof FragmentSpread) { - collectFragmentSpread(parameters, visitedFragments, fields, (FragmentSpread) selection); + collectFragmentSpread(parameters, visitedFragments, fields, (FragmentSpread) selection, incrementalSupport); } } } - private void collectFragmentSpread(FieldCollectorParameters parameters, Set visitedFragments, Map fields, FragmentSpread fragmentSpread) { + private void collectFragmentSpread(FieldCollectorParameters parameters, Set visitedFragments, Map fields, FragmentSpread fragmentSpread, boolean incrementalSupport) { if (visitedFragments.contains(fragmentSpread.getName())) { return; } @@ -95,10 +105,16 @@ private void collectFragmentSpread(FieldCollectorParameters parameters, Set visitedFragments, Map fields, InlineFragment inlineFragment) { + private void collectInlineFragment(FieldCollectorParameters parameters, Set visitedFragments, Map fields, InlineFragment inlineFragment, boolean incrementalSupport) { if (!conditionalNodes.shouldInclude(inlineFragment, parameters.getVariables(), parameters.getGraphQLSchema(), @@ -106,10 +122,16 @@ private void collectInlineFragment(FieldCollectorParameters parameters, Set fields, Field field) { + private void collectField(FieldCollectorParameters parameters, Map fields, Field field, DeferExecution deferExecution) { if (!conditionalNodes.shouldInclude(field, parameters.getVariables(), parameters.getGraphQLSchema(), @@ -119,9 +141,15 @@ private void collectField(FieldCollectorParameters parameters, Map builder.addField(field))); + fields.put(name, curFields.transform(builder -> builder + .addField(field) + .addDeferExecution(deferExecution)) + ); } else { - fields.put(name, MergedField.newMergedField(field).build()); + fields.put(name, MergedField + .newMergedField(field) + .addDeferExecution(deferExecution).build() + ); } } diff --git a/src/main/java/graphql/execution/MergedField.java b/src/main/java/graphql/execution/MergedField.java index 2ce672bb5b..e160bc8622 100644 --- a/src/main/java/graphql/execution/MergedField.java +++ b/src/main/java/graphql/execution/MergedField.java @@ -1,10 +1,13 @@ package graphql.execution; import com.google.common.collect.ImmutableList; +import graphql.ExperimentalApi; import graphql.PublicApi; +import graphql.execution.incremental.DeferExecution; import graphql.language.Argument; import graphql.language.Field; +import javax.annotation.Nullable; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -61,11 +64,13 @@ public class MergedField { private final ImmutableList fields; private final Field singleField; + private final ImmutableList deferExecutions; - private MergedField(ImmutableList fields) { + private MergedField(ImmutableList fields, ImmutableList deferExecutions) { assertNotEmpty(fields); this.fields = fields; this.singleField = fields.get(0); + this.deferExecutions = deferExecutions; } /** @@ -120,6 +125,17 @@ public List getFields() { return fields; } + /** + * TODO: Javadoc + * TODO: should be getIncrementalExecutions? + * @return + */ + @ExperimentalApi + public ImmutableList getDeferExecutions() { + return deferExecutions; + } + + public static Builder newMergedField() { return new Builder(); } @@ -141,12 +157,14 @@ public MergedField transform(Consumer builderConsumer) { public static class Builder { private final ImmutableList.Builder fields = new ImmutableList.Builder<>(); + private final ImmutableList.Builder deferExecutions = new ImmutableList.Builder<>(); private Builder() { } private Builder(MergedField existing) { fields.addAll(existing.getFields()); + deferExecutions.addAll(existing.deferExecutions); } public Builder fields(List fields) { @@ -159,8 +177,20 @@ public Builder addField(Field field) { return this; } + public Builder addDeferExecutions(List deferExecutions) { + this.deferExecutions.addAll(deferExecutions); + return this; + } + + public Builder addDeferExecution(@Nullable DeferExecution deferExecution) { + if(deferExecution != null) { + this.deferExecutions.add(deferExecution); + } + return this; + } + public MergedField build() { - return new MergedField(fields.build()); + return new MergedField(fields.build(), deferExecutions.build()); } } diff --git a/src/main/java/graphql/execution/MergedSelectionSet.java b/src/main/java/graphql/execution/MergedSelectionSet.java index 321a82c7ec..a806af8555 100644 --- a/src/main/java/graphql/execution/MergedSelectionSet.java +++ b/src/main/java/graphql/execution/MergedSelectionSet.java @@ -15,7 +15,7 @@ public class MergedSelectionSet { private final ImmutableMap subFields; - private MergedSelectionSet(Map subFields) { + protected MergedSelectionSet(Map subFields) { this.subFields = ImmutableMap.copyOf(Assert.assertNotNull(subFields)); } diff --git a/src/main/java/graphql/execution/defer/DeferExecutionSupport.java b/src/main/java/graphql/execution/defer/DeferExecutionSupport.java new file mode 100644 index 0000000000..90089f4de6 --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferExecutionSupport.java @@ -0,0 +1,128 @@ +package graphql.execution.defer; + +import graphql.Internal; +import graphql.execution.ExecutionContext; +import graphql.execution.MergedField; +import graphql.execution.ValuesResolver; +import graphql.execution.reactive.SingleSubscriberPublisher; +import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.DelayedIncrementalExecutionResultImpl; +import graphql.language.Directive; +import graphql.language.Field; +import org.reactivestreams.Publisher; + +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static graphql.Directives.DeferDirective; + +/** + * This provides support for @defer directives on fields that mean that results will be sent AFTER + * the main result is sent via a Publisher stream. + */ +@Internal +// TODO: This should be called IncrementalSupport and handle both @defer and @stream +public class DeferExecutionSupport { + private final AtomicBoolean deferDetected = new AtomicBoolean(false); + + private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); + private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); + + private final AtomicInteger pendingCalls = new AtomicInteger(); + + public boolean checkForDeferDirective(MergedField currentField, ExecutionContext executionContext) { + for (Field field : currentField.getFields()) { + List directives = field.getDirectives(DeferDirective.getName()); + // TODO: How to best deal with repeated directives here - @defer/@stream is not a repeated directive + Directive directive = directives.stream().findFirst().orElse(null); + if (directive != null) { + Map argumentValues = ValuesResolver.getArgumentValues( + DeferDirective.getArguments(), + directive.getArguments(), + executionContext.getCoercedVariables(), + executionContext.getGraphQLContext(), + executionContext.getLocale() + ); + return (Boolean) argumentValues.get("if"); + } + } + return false; + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void drainDeferredCalls() { + + DeferredCall deferredCall = deferredCalls.poll(); + + while (deferredCall != null) { + deferredCall.invoke().thenApply(payload -> DelayedIncrementalExecutionResultImpl.newIncrementalExecutionResult() + .incrementalItems(Collections.singletonList(payload)) + .hasNext(pendingCalls.decrementAndGet() != 0) + .build()) + .whenComplete((executionResult, exception) -> { + if (exception != null) { + publisher.offerError(exception); + return; + } + publisher.offer(executionResult); + + if (pendingCalls.get() == 0) { + publisher.noMoreData(); + } + }); + + deferredCall = deferredCalls.poll(); + } + +// boolean isLast = deferredCalls.isEmpty(); +// CompletableFuture future = deferredCall.invoke(); +// future +// .thenApply(payload -> DelayedIncrementalExecutionResultImpl.newIncrementalExecutionResult() +// .incrementalItems(Collections.singletonList(payload)) +// .hasNext(!isLast) +// .build()) +// .whenComplete((executionResult, exception) -> { +// if (exception != null) { +// publisher.offerError(exception); +// return; +// } +// System.out.println("CF completed for: " + executionResult.getIncremental().get(0).getLabel() + ". hasNext: " + executionResult.hasNext()); +// publisher.offer(executionResult); +// drainDeferredCalls(); +// }); + } + + public void enqueue(DeferredCall deferredCall) { + deferDetected.set(true); + deferredCalls.offer(deferredCall); + pendingCalls.incrementAndGet(); + } + + public void enqueue(Collection calls) { + if (!calls.isEmpty()) { + deferDetected.set(true); + deferredCalls.addAll(calls); + pendingCalls.addAndGet(calls.size()); + } + } + + public boolean isDeferDetected() { + return deferDetected.get(); + } + + /** + * When this is called the deferred execution will begin + * + * @return the publisher of deferred results + */ + public Publisher startDeferredCalls() { + drainDeferredCalls(); + return publisher; + } +} diff --git a/src/main/java/graphql/execution/defer/DeferSupport.java b/src/main/java/graphql/execution/defer/DeferSupport.java deleted file mode 100644 index 55261911b3..0000000000 --- a/src/main/java/graphql/execution/defer/DeferSupport.java +++ /dev/null @@ -1,89 +0,0 @@ -package graphql.execution.defer; - -import graphql.DeferredExecutionResult; -import graphql.Internal; -import graphql.execution.ExecutionContext; -import graphql.execution.MergedField; -import graphql.execution.ValuesResolver; -import graphql.execution.reactive.SingleSubscriberPublisher; -import graphql.language.Directive; -import graphql.language.Field; -import org.reactivestreams.Publisher; - -import java.util.Deque; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.atomic.AtomicBoolean; - -import static graphql.Directives.DeferDirective; - -/** - * This provides support for @defer directives on fields that mean that results will be sent AFTER - * the main result is sent via a Publisher stream. - */ -@Internal -// TODO: This should be called IncrementalSupport and handle both @defer and @stream -public class DeferSupport { - - private final AtomicBoolean deferDetected = new AtomicBoolean(false); - private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); - private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); - - public boolean checkForDeferDirective(MergedField currentField, ExecutionContext executionContext) { - for (Field field : currentField.getFields()) { - List directives = field.getDirectives(DeferDirective.getName()); - // TODO: How to best deal with repeated directives here - @defer/@stream is not a repeated directive - Directive directive = directives.stream().findFirst().orElse(null); - if (directive != null) { - Map argumentValues = ValuesResolver.getArgumentValues( - DeferDirective.getArguments(), - directive.getArguments(), - executionContext.getCoercedVariables(), - executionContext.getGraphQLContext(), - executionContext.getLocale() - ); - return (Boolean) argumentValues.get("if"); - } - } - return false; - } - - @SuppressWarnings("FutureReturnValueIgnored") - private void drainDeferredCalls() { - if (deferredCalls.isEmpty()) { - publisher.noMoreData(); - return; - } - DeferredCall deferredCall = deferredCalls.pop(); - CompletableFuture future = deferredCall.invoke(); - future.whenComplete((executionResult, exception) -> { - if (exception != null) { - publisher.offerError(exception); - return; - } - publisher.offer(executionResult); - drainDeferredCalls(); - }); - } - - public void enqueue(DeferredCall deferredCall) { - deferDetected.set(true); - deferredCalls.offer(deferredCall); - } - - public boolean isDeferDetected() { - return deferDetected.get(); - } - - /** - * When this is called the deferred execution will begin - * - * @return the publisher of deferred results - */ - public Publisher startDeferredCalls() { - drainDeferredCalls(); - return publisher; - } -} diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index b52e124d5b..2a25dc256b 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -1,15 +1,20 @@ package graphql.execution.defer; -import graphql.DeferredExecutionResult; -import graphql.DeferredExecutionResultImpl; import graphql.ExecutionResult; import graphql.GraphQLError; import graphql.Internal; +import graphql.execution.Async; import graphql.execution.ResultPath; +import graphql.incremental.DeferPayload; +import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.DelayedIncrementalExecutionResultImpl; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * This represents a deferred call (aka @defer) to get an execution result sometime after @@ -17,27 +22,57 @@ */ @Internal public class DeferredCall { + private final String label; private final ResultPath path; - private final Supplier> call; + private final List>> calls; private final DeferredErrorSupport errorSupport; - public DeferredCall(ResultPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { + public DeferredCall( + String label, + ResultPath path, + List>> calls, + DeferredErrorSupport deferredErrorSupport + ) { + this.label = label; this.path = path; - this.call = call; + this.calls = calls; this.errorSupport = deferredErrorSupport; } - CompletableFuture invoke() { - CompletableFuture future = call.get(); - return future.thenApply(this::transformToDeferredResult); + CompletableFuture invoke() { + Async.CombinedBuilder futures = Async.ofExpectedSize(calls.size()); + + calls.forEach(call -> futures.add(call.get())); + + return futures.await() + .thenApply(this::transformToDeferredPayload); } - private DeferredExecutionResult transformToDeferredResult(ExecutionResult executionResult) { + private DeferPayload transformToDeferredPayload(List fieldWithExecutionResults) { + // TODO: Not sure how/if this errorSupport works List errorsEncountered = errorSupport.getErrors(); - DeferredExecutionResultImpl.Builder builder = DeferredExecutionResultImpl.newDeferredExecutionResult().from(executionResult); - return builder - .addErrors(errorsEncountered) + + Map data = fieldWithExecutionResults.stream() + .collect(Collectors.toMap( + fieldWithExecutionResult -> fieldWithExecutionResult.fieldName, + fieldWithExecutionResult -> fieldWithExecutionResult.executionResult.getData() + )); + + return DeferPayload.newDeferredItem() + .errors(errorsEncountered) .path(path) + .label(label) + .data(data) .build(); } + + public static class FieldWithExecutionResult { + private final String fieldName; + private final ExecutionResult executionResult; + + public FieldWithExecutionResult(String fieldName, ExecutionResult executionResult) { + this.fieldName = fieldName; + this.executionResult = executionResult; + } + } } diff --git a/src/main/java/graphql/execution/defer/DeferredSelectionSet.java b/src/main/java/graphql/execution/defer/DeferredSelectionSet.java new file mode 100644 index 0000000000..21a075e2be --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferredSelectionSet.java @@ -0,0 +1,22 @@ +package graphql.execution.defer; + +import graphql.ExperimentalApi; +import graphql.execution.MergedField; +import graphql.execution.MergedSelectionSet; +import graphql.execution.incremental.DeferExecution; + +import java.util.Map; + +@ExperimentalApi +public class DeferredSelectionSet extends MergedSelectionSet { + private final DeferExecution deferExecution; + + private DeferredSelectionSet(DeferExecution deferExecution, Map subFields) { + super(subFields); + this.deferExecution = deferExecution; + } + + public DeferExecution getDeferExecution() { + return deferExecution; + } +} diff --git a/src/main/java/graphql/execution/incremental/DeferExecution.java b/src/main/java/graphql/execution/incremental/DeferExecution.java new file mode 100644 index 0000000000..e1aa0c1d2f --- /dev/null +++ b/src/main/java/graphql/execution/incremental/DeferExecution.java @@ -0,0 +1,19 @@ +package graphql.execution.incremental; + +import graphql.ExperimentalApi; + +import javax.annotation.Nullable; + +@ExperimentalApi +public class DeferExecution { + private final String label; + + public DeferExecution(String label) { + this.label = label; + } + + @Nullable + public String getLabel() { + return label; + } +} diff --git a/src/main/java/graphql/execution/incremental/IncrementalUtils.java b/src/main/java/graphql/execution/incremental/IncrementalUtils.java new file mode 100644 index 0000000000..b107b575e9 --- /dev/null +++ b/src/main/java/graphql/execution/incremental/IncrementalUtils.java @@ -0,0 +1,66 @@ +package graphql.execution.incremental; + +import graphql.Assert; +import graphql.DeferredExecutionResult; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.CoercedVariables; +import graphql.execution.ExecutionContext; +import graphql.execution.MergedField; +import graphql.execution.ValuesResolver; +import graphql.execution.defer.DeferredCall; +import graphql.execution.reactive.SingleSubscriberPublisher; +import graphql.language.Directive; +import graphql.language.Field; +import graphql.language.NodeUtil; +import graphql.language.TypeName; +import graphql.schema.GraphQLObjectType; +import org.reactivestreams.Publisher; + +import javax.annotation.Nullable; +import java.util.Deque; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +import static graphql.Directives.DeferDirective; + +@Internal +public class IncrementalUtils { + private IncrementalUtils() {} + + // TODO: Refactor this to reduce duplication with IncrementalNodes + public static DeferExecution createDeferExecution( + Map variables, + List directives + ) { + Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); + + if (deferDirective != null) { + Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); + + Object flag = argumentValues.get("if"); + Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + if (!((Boolean) flag)) { + return null; + } + + Object label = argumentValues.get("label"); + + if (label == null) { + return new DeferExecution(null); + } + + Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); + + return new DeferExecution((String) label); + } + + return null; + } +} diff --git a/src/main/java/graphql/incremental/DeferPayload.java b/src/main/java/graphql/incremental/DeferPayload.java index 2327493ef5..58e5ac0994 100644 --- a/src/main/java/graphql/incremental/DeferPayload.java +++ b/src/main/java/graphql/incremental/DeferPayload.java @@ -1,5 +1,6 @@ package graphql.incremental; +import graphql.ExecutionResult; import graphql.ExperimentalApi; import graphql.GraphQLError; @@ -65,6 +66,14 @@ public Builder from(DeferPayload deferredItem) { return this; } + public Builder from(ExecutionResult executionResult) { + this.data = executionResult.getData(); + this.errors = executionResult.getErrors(); + this.extensions = executionResult.getExtensions(); + + return this; + } + public DeferPayload build() { return new DeferPayload(data, this.path, this.label, this.errors, this.extensions); } diff --git a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java index 3ba520dab7..abce52f359 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java @@ -35,4 +35,10 @@ public interface DelayedIncrementalExecutionResult { */ @Nullable Map getExtensions(); + + /** + * TODO: Javadoc + * @return + */ + Map toSpecification(); } diff --git a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java index 5434db8798..3ad40f766f 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java @@ -3,8 +3,10 @@ import graphql.ExperimentalApi; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @ExperimentalApi public class DelayedIncrementalExecutionResultImpl implements DelayedIncrementalExecutionResult { @@ -33,6 +35,24 @@ public Map getExtensions() { return this.extensions; } + @Override + public Map toSpecification() { + Map result = new LinkedHashMap<>(); + result.put("hasNext", hasNext); + + if (extensions != null) { + result.put("extensions", extensions); + } + + if(incrementalItems != null) { + result.put("incremental", incrementalItems.stream() + .map(IncrementalPayload::toSpecification) + .collect(Collectors.toList())); + } + + return result; + } + /** * @return a {@link Builder} that can be used to create an instance of {@link DelayedIncrementalExecutionResultImpl} */ diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java index 5b16cdaf50..8bf8ace96f 100644 --- a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java @@ -1,5 +1,6 @@ package graphql.incremental; +import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.ExperimentalApi; import org.reactivestreams.Publisher; @@ -49,6 +50,10 @@ public static Builder newIncrementalExecutionResult() { return new Builder(); } + public static Builder fromExecutionResult(ExecutionResult executionResult) { + return new Builder().from(executionResult); + } + @Override public Map toSpecification() { Map map = new LinkedHashMap<>(super.toSpecification()); diff --git a/src/main/java/graphql/incremental/IncrementalPayload.java b/src/main/java/graphql/incremental/IncrementalPayload.java index a3ddd55826..76b1fbb405 100644 --- a/src/main/java/graphql/incremental/IncrementalPayload.java +++ b/src/main/java/graphql/incremental/IncrementalPayload.java @@ -1,5 +1,6 @@ package graphql.incremental; +import graphql.ExecutionResult; import graphql.ExperimentalApi; import graphql.GraphQLError; import graphql.execution.ResultPath; @@ -82,6 +83,7 @@ protected Object errorsToSpec(List errors) { return errors.stream().map(GraphQLError::toSpecification).collect(toList()); } + protected static abstract class Builder> { protected List path; protected String label; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 3a8d36af08..bc2eede068 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -10,7 +10,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -66,7 +66,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -262,12 +262,12 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } - public void addDeferExecutions(Collection deferExecutions) { + public void addDeferExecutions(Collection deferExecutions) { this.deferExecutions.addAll(deferExecutions); } @@ -477,11 +477,11 @@ public ExecutableNormalizedField getParent() { } /** - * @return the {@link DeferExecution}s associated with this {@link ExecutableNormalizedField}. - * @see DeferExecution + * @return the {@link NormalizedDeferExecution}s associated with this {@link ExecutableNormalizedField}. + * @see NormalizedDeferExecution */ @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -612,7 +612,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -683,7 +683,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 9367c1d58d..861775e3e7 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -28,9 +28,8 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; -import graphql.language.TypeName; import graphql.language.VariableDefinition; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; @@ -633,12 +632,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferExecution collectedDeferExecution = collectedField.deferExecution; + NormalizedDeferExecution collectedDeferExecution = collectedField.deferExecution; if (collectedDeferExecution != null) { deferExecutionsBuilder.add(collectedDeferExecution); @@ -646,7 +645,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build(); + Set deferExecutions = deferExecutionsBuilder.build(); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -664,7 +663,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - Set filteredDeferExecutions = deferExecutions.stream() + Set filteredDeferExecutions = deferExecutions.stream() .filter(filterExecutionsFromType(objectType)) .collect(toCollection(LinkedHashSet::new)); @@ -673,7 +672,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { + private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { String objectTypeName = objectType.getName(); return deferExecution -> deferExecution.getPossibleTypes() .stream() @@ -681,9 +680,9 @@ private static Predicate filterExecutionsFromType(GraphQLObjectT .anyMatch(objectTypeName::equals); } - private Set listDuplicatedLabels(Collection deferExecutions) { + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() - .map(DeferExecution::getLabel) + .map(NormalizedDeferExecution::getLabel) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() @@ -697,7 +696,7 @@ private void collectFromSelectionSet(SelectionSet selectionSet, List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferExecution deferExecution + NormalizedDeferExecution deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { @@ -731,9 +730,8 @@ private void collectFragmentSpread(List result, GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(this.graphQLSchema.getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition); - DeferExecution newDeferExecution = buildDeferExecution( + NormalizedDeferExecution newDeferExecution = buildDeferExecution( fragmentSpread.getDirectives(), - fragmentDefinition.getTypeCondition(), newPossibleObjects); collectFromSelectionSet(fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); @@ -756,18 +754,16 @@ private void collectInlineFragment(List result, } - DeferExecution newDeferExecution = buildDeferExecution( + NormalizedDeferExecution newDeferExecution = buildDeferExecution( inlineFragment.getDirectives(), - inlineFragment.getTypeCondition(), newPossibleObjects ); collectFromSelectionSet(inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } - private DeferExecution buildDeferExecution( + private NormalizedDeferExecution buildDeferExecution( List directives, - TypeName typeCondition, Set newPossibleObjects) { if(!options.deferSupport) { return null; @@ -776,7 +772,6 @@ private DeferExecution buildDeferExecution( return incrementalNodes.createDeferExecution( this.coercedVariableValues.toMap(), directives, - typeCondition, newPossibleObjects ); } @@ -785,7 +780,7 @@ private void collectField(List result, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferExecution deferExecution + NormalizedDeferExecution deferExecution ) { if (!conditionalNodes.shouldInclude(field, this.coercedVariableValues.toMap(), @@ -852,9 +847,9 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; - DeferExecution deferExecution; + NormalizedDeferExecution deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, NormalizedDeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; @@ -885,9 +880,9 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { private static class CollectedFieldGroup { Set objectTypes; Set fields; - Set deferExecutions; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; this.deferExecutions = deferExecutions; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 051d4655de..22e1340f90 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,7 +23,7 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -277,7 +277,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -489,9 +489,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferExecution deferExecution; + private final NormalizedDeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 025b9333a0..79574658a2 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -7,10 +7,8 @@ import graphql.execution.ValuesResolver; import graphql.language.Directive; import graphql.language.NodeUtil; -import graphql.language.TypeName; import graphql.schema.GraphQLObjectType; -import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; @@ -21,10 +19,9 @@ @Internal public class IncrementalNodes { - public DeferExecution createDeferExecution( + public NormalizedDeferExecution createDeferExecution( Map variables, List directives, - @Nullable TypeName targetType, Set possibleTypes ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -42,12 +39,12 @@ public DeferExecution createDeferExecution( Object label = argumentValues.get("label"); if (label == null) { - return new DeferExecution(null, possibleTypes); + return new NormalizedDeferExecution(null, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label, possibleTypes); + return new NormalizedDeferExecution((String) label, possibleTypes); } return null; diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java similarity index 96% rename from src/main/java/graphql/normalized/incremental/DeferExecution.java rename to src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java index 71488b03ca..82d87153c9 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java @@ -103,11 +103,11 @@ * */ @ExperimentalApi -public class DeferExecution { +public class NormalizedDeferExecution { private final String label; private final Set possibleTypes; - public DeferExecution(@Nullable String label, Set possibleTypes) { + public NormalizedDeferExecution(@Nullable String label, Set possibleTypes) { this.label = label; this.possibleTypes = possibleTypes; } diff --git a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy index 7245f24ee9..8401a3cf1e 100644 --- a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy +++ b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy @@ -1,20 +1,22 @@ package graphql.execution.defer import graphql.DeferredExecutionResult +import graphql.incremental.DeferPayload +import graphql.incremental.DelayedIncrementalExecutionResult import org.reactivestreams.Publisher import org.reactivestreams.Subscriber import org.reactivestreams.Subscription import java.util.concurrent.atomic.AtomicBoolean -class CapturingSubscriber implements Subscriber { +class CapturingSubscriber implements Subscriber { Subscription subscription AtomicBoolean finished = new AtomicBoolean() Throwable throwable - List executionResults = [] + List executionResults = [] List executionResultData = [] - AtomicBoolean subscribeTo(Publisher publisher) { + AtomicBoolean subscribeTo(Publisher publisher) { publisher.subscribe(this) return finished } @@ -27,9 +29,12 @@ class CapturingSubscriber implements Subscriber { } @Override - void onNext(DeferredExecutionResult executionResult) { - executionResults.add(executionResult) - executionResultData.add(executionResult.getData()) + void onNext(DelayedIncrementalExecutionResult incrementalExecutionResult) { + executionResults.add(incrementalExecutionResult) + executionResultData.addAll(incrementalExecutionResult.incremental + // We only support defer (and not stream) for now + .collect {((DeferPayload) it).data} + ) subscription.request(1) } diff --git a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy index 7f592c6db4..cfb2d37cd1 100644 --- a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy +++ b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy @@ -22,7 +22,7 @@ class DeferSupportTest extends Specification { def "emits N deferred calls with order preserved"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() deferSupport.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last deferSupport.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first @@ -48,7 +48,7 @@ class DeferSupportTest extends Specification { def "calls within calls are enqueued correctly"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "A", "a", 100, "/a")) deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "B", "b", 50, "/b")) deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "C", "c", 10, "/c")) @@ -78,7 +78,7 @@ class DeferSupportTest extends Specification { def "stops at first exception encountered"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() deferSupport.enqueue(offThread("A", 100, "/field/path")) deferSupport.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception deferSupport.enqueue(offThread("C", 10, "/field/path")) @@ -114,7 +114,7 @@ class DeferSupportTest extends Specification { def "you can cancel the subscription"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() deferSupport.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last deferSupport.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first @@ -141,7 +141,7 @@ class DeferSupportTest extends Specification { def "you cant subscribe twice"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() deferSupport.enqueue(offThread("A", 100, "/field/path")) deferSupport.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first @@ -161,7 +161,7 @@ class DeferSupportTest extends Specification { def "indicates of there any defers present"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() when: def deferPresent1 = deferSupport.isDeferDetected() @@ -179,7 +179,7 @@ class DeferSupportTest extends Specification { def "detects @defer directive"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() when: def noDirectivePresent = deferSupport.checkForDeferDirective(mergedField([ @@ -202,7 +202,7 @@ class DeferSupportTest extends Specification { def "detects @defer directive can be controlled via if"() { given: - def deferSupport = new DeferSupport() + def deferSupport = new DeferExecutionSupport() when: def ifArg = new Argument("if", new BooleanValue(false)) @@ -259,7 +259,7 @@ class DeferSupportTest extends Specification { } private - static DeferredCall offThreadCallWithinCall(DeferSupport deferSupport, String dataParent, String dataChild, int sleepTime, String path) { + static DeferredCall offThreadCallWithinCall(DeferExecutionSupport deferSupport, String dataParent, String dataChild, int sleepTime, String path) { def callSupplier = { CompletableFuture.supplyAsync({ Thread.sleep(sleepTime) diff --git a/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy new file mode 100644 index 0000000000..455a96c5e4 --- /dev/null +++ b/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy @@ -0,0 +1,595 @@ +package graphql.execution.incremental + +import graphql.DeferredExecutionResult +import graphql.Directives +import graphql.ErrorType +import graphql.ExecutionInput +import graphql.ExecutionResult +import graphql.GraphQL +import graphql.TestUtil +import graphql.execution.defer.CapturingSubscriber +import graphql.incremental.DelayedIncrementalExecutionResult +import graphql.incremental.IncrementalExecutionResult +import graphql.incremental.IncrementalExecutionResultImpl +import graphql.schema.DataFetcher +import graphql.schema.idl.RuntimeWiring +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import org.awaitility.Awaitility +import org.reactivestreams.Publisher +import spock.lang.Specification +import spock.lang.Unroll + +import java.util.concurrent.CompletableFuture + +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring + +class DeferSupportIntegrationTest extends Specification { + def schemaSpec = ''' + type Query { + post : Post + } + + type Post { + id: ID! + # takes 100ms to resolve + summary : String + # takes 300ms to resolve + text : String + } + ''' + + GraphQL graphQL = null + + private static DataFetcher resolve(Object value) { + return (env) -> value + } + + private static DataFetcher resolve(Object value, Integer sleepMs) { + return (env) -> CompletableFuture.supplyAsync { + println("" + new Date().getTime() + "|calling df for: " + value + ". Sleeping: " + sleepMs + ". Thread name: " + Thread.currentThread().name) + Thread.sleep(sleepMs) + println("" + new Date().getTime() + "|value resolved: " + value) + return value + } + } + + void setup() { + def runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(newTypeWiring("Query").dataFetcher("post", resolve([id: "1001"]))) + .type(newTypeWiring("Post").dataFetcher("summary", resolve("A summary", 10))) + .type(newTypeWiring("Post").dataFetcher("text", resolve("The full text", 1000))) + .build() + + def schema = TestUtil.schema(schemaSpec, runtimeWiring) + .transform({ builder -> builder.additionalDirective(Directives.DeferDirective) }) + this.graphQL = GraphQL.newGraphQL(schema).build() + } + + def "simple defer"() { + def query = ''' + query { + post { + id + ... @defer { + summary + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary"] + ] + ] + ] + ] + } + + def "simple defer with label"() { + def query = ''' + query { + post { + id + ... @defer(label: "summary-defer") { + summary + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + label: "summary-defer", + data : [summary: "A summary"] + ] + ] + ] + ] + } + + @Unroll + def "defer with 'if: #ifValue' "() { + def query = """ + query { + post { + id + ... @defer(if: $ifValue) { + summary + } + } + } + """ + + when: + ExecutionResult executionResult = executeQuery(query) + + then: + if (ifValue) { + assert executionResult instanceof IncrementalExecutionResultImpl + + assert executionResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + def incrementalResults = getIncrementalResults(executionResult) + + assert incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary"] + ] + ] + ] + ] + } else { + assert !(executionResult instanceof IncrementalExecutionResult) + + assert executionResult.toSpecification() == [ + data: [post: [id: "1001", summary: "A summary"]], + ] + } + + where: + ifValue << [true, false] + } + + @Unroll + def "defer with 'if: #ifValue' passed as variable "() { + def query = """ + query(\$ifVar: Boolean!) { + post { + id + ... @defer(if: \$ifVar) { + summary + } + } + } + """ + + when: + ExecutionResult executionResult = executeQuery(query, [ifVar: ifValue]) + + then: + if (ifValue) { + assert executionResult instanceof IncrementalExecutionResultImpl + + assert executionResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + def incrementalResults = getIncrementalResults(executionResult) + + assert incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary"] + ] + ] + ] + ] + } else { + assert !(executionResult instanceof IncrementalExecutionResult) + + assert executionResult.toSpecification() == [ + data: [post: [id: "1001", summary: "A summary"]], + ] + } + + where: + ifValue << [true, false] + } + + def "2 fields deferred together"() { + def query = ''' + query { + post { + id + ... @defer { + summary + text + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary", text: "The full text"] + ] + ] + ] + ] + } + + def "2 fields deferred independently"() { + def query = ''' + query { + post { + id + ... @defer(label: "summary-defer") { + summary + } + ... @defer(label: "text-defer") { + text + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + label: "summary-defer", + path: ["post"], + data: [summary: "A summary"] + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + label: "text-defer", + path: ["post"], + data: [text: "The full text"] + ] + ] + ] + ] + } + + def "multiple defers on same field"() { + + def query = ''' + query { + post { + sentAt + ... @defer { + postText + } + ... @defer(label: "defer-outer") { + postText + ... @defer(label: "defer-inner") { + postText + } + } + } + } + ''' + + when: + def initialResult = executeQuery(query) + + then: + initialResult.errors.isEmpty() + initialResult.data == ["post": ["postText": "post_data"]] + + throw new IllegalStateException("Need to assert data fetcher for field is called just once") + +// when: +// +// Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher +// +// def subscriber = new CapturingSubscriber() +// subscriber.subscribeTo(deferredResultStream) +// Awaitility.await().untilTrue(subscriber.finished) +// +// List resultList = subscriber.executionResults +// +// then: +// +// assertDeferredData(resultList) + } + + def "test defer support end to end"() { + + def query = ''' + query { + post { + postText + + ... @defer { + a :comments(sleepTime:200) { + commentText + } + } + + ... @defer { + b : reviews(sleepTime:100) { + reviewText + ... @defer { + comments(prefix : "b_") { + commentText + } + } + } + } + + ... @defer { + c: reviews { + goes { + bang + } + } + } + } + } + ''' + + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + + then: + initialResult.errors.isEmpty() + initialResult.data == ["post": ["postText": "post_data", a: null, b: null, c: null]] + + when: + + Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream) + Awaitility.await().untilTrue(subscriber.finished) + + List resultList = subscriber.executionResults + + then: + + assertDeferredData(resultList) + } + + def "test defer support keeps the fields named correctly when interspersed in the query"() { + + def query = ''' + query { + post { + interspersedA: echo(text:"before a:") + + a: comments(sleepTime:200) @defer { + commentText + } + + interspersedB: echo(text:"before b:") + + b : reviews(sleepTime:100) @defer { + reviewText + comments(prefix : "b_") @defer { + commentText + } + } + + interspersedC: echo(text:"before c:") + + c: reviews @defer { + goes { + bang + } + } + + interspersedD: echo(text:"after c:") + } + } + ''' + + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + + then: + initialResult.errors.isEmpty() + initialResult.data == ["post": [ + "interspersedA": "before a:", + "a" : null, + "interspersedB": "before b:", + "b" : null, + "interspersedC": "before c:", + "c" : null, + "interspersedD": "after c:", + ]] + + when: + + Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream); + Awaitility.await().untilTrue(subscriber.finished) + + List resultList = subscriber.executionResults + + then: + + assertDeferredData(resultList) + } + + def assertDeferredData(ArrayList resultList) { + resultList.size() == 6 + + assert resultList[0].data == [[commentText: "comment0"], [commentText: "comment1"], [commentText: "comment2"]] + assert resultList[0].errors == [] + assert resultList[0].path == ["post", "a"] + + assert resultList[1].data == [[reviewText: "review0", comments: null], [reviewText: "review1", comments: null], [reviewText: "review2", comments: null]] + assert resultList[1].errors == [] + assert resultList[1].path == ["post", "b"] + + // exceptions in here + assert resultList[2].errors.size() == 3 + assert resultList[2].errors[0].getMessage() == "Exception while fetching data (/post/c[0]/goes/bang) : Bang!" + assert resultList[2].errors[1].getMessage() == "Exception while fetching data (/post/c[1]/goes/bang) : Bang!" + assert resultList[2].errors[2].getMessage() == "Exception while fetching data (/post/c[2]/goes/bang) : Bang!" + assert resultList[2].path == ["post", "c"] + + // sub defers are sent in encountered order + assert resultList[3].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] + assert resultList[3].errors == [] + assert resultList[3].path == ["post", "b", 0, "comments"] + + assert resultList[4].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] + assert resultList[4].errors == [] + assert resultList[4].path == ["post", "b", 1, "comments"] + + assert resultList[5].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] + assert resultList[5].errors == [] + assert resultList[5].path == ["post", "b", 2, "comments"] + + true + } + + def "nonNull types are not allowed"() { + + def query = ''' + { + mandatoryReviews @defer # nulls are not allowed + { + reviewText + } + } + ''' + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + then: + initialResult.errors.size() == 1 + initialResult.errors[0].errorType == ErrorType.ValidationError + (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveOnNonNullField + + } + + def "mutations cant have defers"() { + + def query = ''' + mutation { + mutate(arg : "go") @defer + } + ''' + when: + def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) + then: + initialResult.errors.size() == 1 + initialResult.errors[0].errorType == ErrorType.ValidationError + (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveNotOnQueryOperation + } + + private ExecutionResult executeQuery(String query) { + return this.executeQuery(query, true, [:]) + } + + private ExecutionResult executeQuery(String query, Map variables) { + return this.executeQuery(query, true, variables) + } + + private ExecutionResult executeQuery(String query, boolean incrementalSupport, Map variables) { + return graphQL.execute( + ExecutionInput.newExecutionInput() + .incrementalSupport(incrementalSupport) + .query(query) + .variables(variables) + .build() + ) + } + + private static List> getIncrementalResults(IncrementalExecutionResult initialResult) { + Publisher deferredResultStream = initialResult.incrementalItemPublisher + + def subscriber = new CapturingSubscriber() + subscriber.subscribeTo(deferredResultStream) + Awaitility.await().untilTrue(subscriber.finished) + + return subscriber.executionResults + .collect { it.toSpecification() } + } +} From 36d033001cb4799c748e381b0137a13cfbcfec75 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 19 Jan 2024 17:33:04 +1100 Subject: [PATCH 102/393] Fixed a race condition and added more tests --- .../execution/AsyncExecutionStrategy.java | 39 +-- .../java/graphql/execution/Execution.java | 6 +- .../defer/DeferExecutionSupport.java | 58 ++-- .../graphql/execution/defer/DeferredCall.java | 1 + .../DeferSupportIntegrationTest.groovy | 280 ++++++++++++++++-- 5 files changed, 287 insertions(+), 97 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 1d1f53c1fe..0d9305032b 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -15,7 +15,6 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.schema.GraphQLFieldDefinition; import graphql.util.FpKit; -import graphql.util.Pair; import java.util.LinkedHashMap; import java.util.List; @@ -193,7 +192,8 @@ private static class SomethingDefer { private SomethingDefer( MergedSelectionSet mergedSelectionSet, ExecutionStrategyParameters parameters, - ExecutionContext executionContext, BiFunction> resolveFieldWithInfoFn + ExecutionContext executionContext, + BiFunction> resolveFieldWithInfoFn ) { this.executionContext = executionContext; this.resolveFieldWithInfoFn = resolveFieldWithInfoFn; @@ -233,11 +233,11 @@ private Set createCalls() { builder.deferredErrorSupport(errorSupport) .field(currentField) .fields(mergedSelectionSet) - .parent(null); // this is a break in the parent -> child chain - its a new start effectively + .path(builder.path.segment(currentField.getName())) + .parent(null); // this is a break in the parent -> child chain - it's a new start effectively } ); - return (Supplier>) () -> resolveFieldWithInfoFn .apply(executionContext, callParameters) .thenCompose(FieldValueInfo::getFieldValue) @@ -257,36 +257,5 @@ private Set createCalls() { .collect(Collectors.toSet()); } - @SuppressWarnings("FutureReturnValueIgnored") - private Supplier> deferredExecutionResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - return () -> { -// GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); - // TODO: freis: This is suddenly not needed anymore -// GraphQLObjectType fieldContainer = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); - -// Instrumentation instrumentation = executionContext.getInstrumentation(); - -// Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); - -// InstrumentationContext fieldCtx = instrumentation.beginDeferredField( -// new InstrumentationDeferredFieldParameters(executionContext, executionStepInfo, parameters), -// executionContext.getInstrumentationState() -// ); - - CompletableFuture result = new CompletableFuture<>(); -// fieldCtx.onDispatched(result); - CompletableFuture fieldValueInfoFuture = resolveFieldWithInfoFn.apply(executionContext, parameters); - - fieldValueInfoFuture.whenComplete((fieldValueInfo, throwable) -> { - // TODO: -// fieldCtx.onFieldValueInfo(fieldValueInfo); - - CompletableFuture execResultFuture = fieldValueInfo.getFieldValue(); -// execResultFuture = execResultFuture.whenComplete(fieldCtx::onCompleted); - Async.copyResults(execResultFuture, result); - }); - return result; - }; - } } } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 3871e5e8ae..ba9894369e 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -141,7 +141,11 @@ private CompletableFuture executeOperation(ExecutionContext exe .graphQLContext(graphQLContext) .build(); - MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDefinition.getSelectionSet()); + MergedSelectionSet fields = fieldCollector.collectFields( + collectorParameters, + operationDefinition.getSelectionSet(), + executionContext.getExecutionInput().isIncrementalSupport() + ); ResultPath path = ResultPath.rootPath(); ExecutionStepInfo executionStepInfo = newExecutionStepInfo().type(operationRootType).path(path).build(); diff --git a/src/main/java/graphql/execution/defer/DeferExecutionSupport.java b/src/main/java/graphql/execution/defer/DeferExecutionSupport.java index 90089f4de6..504e208b39 100644 --- a/src/main/java/graphql/execution/defer/DeferExecutionSupport.java +++ b/src/main/java/graphql/execution/defer/DeferExecutionSupport.java @@ -6,9 +6,9 @@ import graphql.execution.ValuesResolver; import graphql.execution.reactive.SingleSubscriberPublisher; import graphql.incremental.DelayedIncrementalExecutionResult; -import graphql.incremental.DelayedIncrementalExecutionResultImpl; import graphql.language.Directive; import graphql.language.Field; +import graphql.util.LockKit; import org.reactivestreams.Publisher; import java.util.Collection; @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicInteger; import static graphql.Directives.DeferDirective; +import static graphql.incremental.DelayedIncrementalExecutionResultImpl.newIncrementalExecutionResult; /** * This provides support for @defer directives on fields that mean that results will be sent AFTER @@ -30,11 +31,11 @@ // TODO: This should be called IncrementalSupport and handle both @defer and @stream public class DeferExecutionSupport { private final AtomicBoolean deferDetected = new AtomicBoolean(false); - private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); - private final AtomicInteger pendingCalls = new AtomicInteger(); + private final LockKit.ReentrantLock publisherLock = new LockKit.ReentrantLock(); + public boolean checkForDeferDirective(MergedField currentField, ExecutionContext executionContext) { for (Field field : currentField.getFields()) { @@ -57,45 +58,44 @@ public boolean checkForDeferDirective(MergedField currentField, ExecutionContext @SuppressWarnings("FutureReturnValueIgnored") private void drainDeferredCalls() { - DeferredCall deferredCall = deferredCalls.poll(); while (deferredCall != null) { - deferredCall.invoke().thenApply(payload -> DelayedIncrementalExecutionResultImpl.newIncrementalExecutionResult() - .incrementalItems(Collections.singletonList(payload)) - .hasNext(pendingCalls.decrementAndGet() != 0) - .build()) - .whenComplete((executionResult, exception) -> { + deferredCall.invoke() + .whenComplete((payload, exception) -> { if (exception != null) { publisher.offerError(exception); return; } - publisher.offer(executionResult); - if (pendingCalls.get() == 0) { + publisherLock.lock(); + final int remainingCalls; + + try { + // The assigment of `remainingCalls` and `publisher.offer` need to be synchronized to ensure + // `hasNext` is `false` precisely on the last event offered to the publisher. + remainingCalls = pendingCalls.decrementAndGet(); + + DelayedIncrementalExecutionResult executionResult = newIncrementalExecutionResult() + .incrementalItems(Collections.singletonList(payload)) + .hasNext(remainingCalls != 0) + .build(); + + publisher.offer(executionResult); + } finally { + publisherLock.unlock(); + } + ; + + if (remainingCalls == 0) { publisher.noMoreData(); + } else { + // Nested calls were added, let's try to drain the queue again. + drainDeferredCalls(); } }); - deferredCall = deferredCalls.poll(); } - -// boolean isLast = deferredCalls.isEmpty(); -// CompletableFuture future = deferredCall.invoke(); -// future -// .thenApply(payload -> DelayedIncrementalExecutionResultImpl.newIncrementalExecutionResult() -// .incrementalItems(Collections.singletonList(payload)) -// .hasNext(!isLast) -// .build()) -// .whenComplete((executionResult, exception) -> { -// if (exception != null) { -// publisher.offerError(exception); -// return; -// } -// System.out.println("CF completed for: " + executionResult.getIncremental().get(0).getLabel() + ". hasNext: " + executionResult.hasNext()); -// publisher.offer(executionResult); -// drainDeferredCalls(); -// }); } public void enqueue(DeferredCall deferredCall) { diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index 2a25dc256b..b1e68444c3 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -10,6 +10,7 @@ import graphql.incremental.DelayedIncrementalExecutionResultImpl; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; diff --git a/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy index 455a96c5e4..3d87e432f4 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy @@ -12,6 +12,7 @@ import graphql.incremental.DelayedIncrementalExecutionResult import graphql.incremental.IncrementalExecutionResult import graphql.incremental.IncrementalExecutionResultImpl import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import graphql.schema.idl.RuntimeWiring import graphql.validation.ValidationError import graphql.validation.ValidationErrorType @@ -28,37 +29,65 @@ class DeferSupportIntegrationTest extends Specification { def schemaSpec = ''' type Query { post : Post + hello: String } type Post { id: ID! - # takes 100ms to resolve summary : String - # takes 300ms to resolve text : String + latestComment: Comment + comments: [Comment] } + + type Comment { + title: String + content: String + author: Person + } + + type Person { + name: String + avatar: String + } + ''' GraphQL graphQL = null private static DataFetcher resolve(Object value) { - return (env) -> value + return resolve(value, 0) } private static DataFetcher resolve(Object value, Integer sleepMs) { - return (env) -> CompletableFuture.supplyAsync { - println("" + new Date().getTime() + "|calling df for: " + value + ". Sleeping: " + sleepMs + ". Thread name: " + Thread.currentThread().name) - Thread.sleep(sleepMs) - println("" + new Date().getTime() + "|value resolved: " + value) - return value + return new DataFetcher() { + boolean executed = false; + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + if(executed) { + throw new IllegalStateException("This data fetcher can run only once") + } + executed = true; + return CompletableFuture.supplyAsync { + Thread.sleep(sleepMs) + return value + } + } } } void setup() { def runtimeWiring = RuntimeWiring.newRuntimeWiring() - .type(newTypeWiring("Query").dataFetcher("post", resolve([id: "1001"]))) + .type(newTypeWiring("Query") + .dataFetcher("post", resolve([id: "1001"])) + .dataFetcher("hello", resolve("world")) + ) .type(newTypeWiring("Post").dataFetcher("summary", resolve("A summary", 10))) - .type(newTypeWiring("Post").dataFetcher("text", resolve("The full text", 1000))) + .type(newTypeWiring("Post").dataFetcher("text", resolve("The full text", 100))) + .type(newTypeWiring("Post").dataFetcher("latestComment", resolve([title: "Comment title"], 10))) + .type(newTypeWiring("Comment").dataFetcher("content", resolve("Full content", 100))) + .type(newTypeWiring("Comment").dataFetcher("author", resolve([name: "Author name"], 10))) + .type(newTypeWiring("Person").dataFetcher("avatar", resolve("Avatar image", 100))) .build() def schema = TestUtil.schema(schemaSpec, runtimeWiring) @@ -143,6 +172,47 @@ class DeferSupportIntegrationTest extends Specification { ] } + def "simple defer with fragment definition"() { + def query = ''' + query { + post { + id + ... PostData @defer + } + } + + fragment PostData on Post { + summary + text + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + data : [summary: "A summary", text: "The full text"] + ] + ] + ] + ] + } + @Unroll def "defer with 'if: #ifValue' "() { def query = """ @@ -333,19 +403,157 @@ class DeferSupportIntegrationTest extends Specification { ] } + def "defer result in initial result being empty object"() { + def query = ''' + query { + post { + ... @defer { + summary + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [:]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary"] + ] + ] + ] + ] + } + + def "defer on top level field"() { + def query = ''' + query { + hello + ... @defer { + post { + id + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [hello: "world"], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: [], + data: [post: [id: "1001"]] + ] + ] + ] + ] + } + + def "nested defers"() { + def query = ''' + query { + ... @defer { + post { + id + ... @defer { + summary + latestComment { + title + ... @defer { + content + } + } + } + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [:], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path: [], + data: [post: [id: "1001"]] + ] + ] + ], + [ + hasNext : true, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary", latestComment: [title: "Comment title"]] + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path: ["post", "latestComment"], + data: [content: "Full content"] + ] + ] + ] + ] + } + def "multiple defers on same field"() { def query = ''' query { post { - sentAt ... @defer { - postText + summary } ... @defer(label: "defer-outer") { - postText + summary ... @defer(label: "defer-inner") { - postText + summary } } } @@ -353,27 +561,35 @@ class DeferSupportIntegrationTest extends Specification { ''' when: + def initialResult = executeQuery(query) then: - initialResult.errors.isEmpty() - initialResult.data == ["post": ["postText": "post_data"]] - - throw new IllegalStateException("Need to assert data fetcher for field is called just once") - -// when: -// -// Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher -// -// def subscriber = new CapturingSubscriber() -// subscriber.subscribeTo(deferredResultStream) -// Awaitility.await().untilTrue(subscriber.finished) -// -// List resultList = subscriber.executionResults -// -// then: -// -// assertDeferredData(resultList) + initialResult.toSpecification() == [ + data : [post: [:]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + // Ordering is non-deterministic, so we assert on the things we know are going to be true. + + incrementalResults.size() == 3 + // only the last payload has "hasNext=true" + incrementalResults[0].hasNext == true + incrementalResults[1].hasNext == true + incrementalResults[2].hasNext == false + + // every payload has only 1 incremental item, and the data is the same for all of them + incrementalResults.every { it.incremental.size == 1 } + incrementalResults.every { it.incremental[0].data == [summary: "A summary"] } + + // "label" is different for every payload + incrementalResults.any { it.incremental[0].label == null } + incrementalResults.any { it.incremental[0].label == "defer-inner" } + incrementalResults.any { it.incremental[0].label == "defer-outer" } } def "test defer support end to end"() { From 348515db01982a047b277ee396664ee6e632caae Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 19 Jan 2024 17:51:10 +1100 Subject: [PATCH 103/393] Memoize field value resolution --- .../execution/AsyncExecutionStrategy.java | 48 ++++-------- .../graphql/execution/ExecutionStrategy.java | 7 -- .../defer/DeferExecutionSupport.java | 33 +------- .../execution/defer/DeferSupportTest.groovy | 75 ------------------- ...ferExecutionSupportIntegrationTest.groovy} | 2 +- 5 files changed, 17 insertions(+), 148 deletions(-) rename src/test/groovy/graphql/execution/incremental/{DeferSupportIntegrationTest.groovy => DeferExecutionSupportIntegrationTest.groovy} (99%) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 0d9305032b..d7493308bd 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableSet; import graphql.ExecutionResult; import graphql.PublicApi; -import graphql.execution.defer.DeferExecutionSupport; import graphql.execution.defer.DeferredCall; import graphql.execution.defer.DeferredErrorSupport; import graphql.execution.incremental.DeferExecution; @@ -16,6 +15,7 @@ import graphql.schema.GraphQLFieldDefinition; import graphql.util.FpKit; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -86,7 +86,6 @@ public CompletableFuture execute(ExecutionContext executionCont if (somethingDefer.isDeferredField(currentField)) { executionStrategyCtx.onDeferredField(currentField); -// future = resolveFieldWithInfoToNull(executionContext, newParameters); } else { future = resolveFieldWithInfo(executionContext, newParameters); futures.add(future); @@ -122,33 +121,6 @@ public CompletableFuture execute(ExecutionContext executionCont return overallResult; } - private boolean isDeferred(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedField currentField) { - DeferExecutionSupport deferSupport = executionContext.getDeferSupport(); - - if (currentField.getDeferExecutions() != null && !currentField.getDeferExecutions().isEmpty()) { - DeferredErrorSupport errorSupport = new DeferredErrorSupport(); - - // with a deferred field we are really resetting where we execute from, that is from this current field onwards - Map fields = new LinkedHashMap<>(); - fields.put(currentField.getName(), currentField); - - ExecutionStrategyParameters callParameters = parameters.transform(builder -> - { - MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); - builder.deferredErrorSupport(errorSupport) - .field(currentField) - .fields(mergedSelectionSet) - .parent(null); // this is a break in the parent -> child chain - its a new start effectively - } - ); - -// DeferredCall call = new DeferredCall(null /* TODO extract label somehow*/, parameters.getPath(), deferredExecutionResult(executionContext, callParameters), errorSupport); -// deferSupport.enqueue(call); - return true; - } - return false; - } - @SuppressWarnings("FutureReturnValueIgnored") private Supplier> deferredExecutionResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { return () -> { @@ -188,6 +160,7 @@ private static class SomethingDefer { private final ExecutionStrategyParameters parameters; private final ExecutionContext executionContext; private final BiFunction> resolveFieldWithInfoFn; + private final Map>> dfCache = new HashMap<>(); private SomethingDefer( MergedSelectionSet mergedSelectionSet, @@ -217,7 +190,8 @@ private boolean isDeferredField(MergedField mergedField) { } private Set createCalls() { - return deferExecutionToFields.keySet().stream().map(deferExecution -> { + return deferExecutionToFields.keySet().stream() + .map(deferExecution -> { DeferredErrorSupport errorSupport = new DeferredErrorSupport(); List mergedFields = deferExecutionToFields.get(deferExecution); @@ -238,10 +212,16 @@ private Set createCalls() { } ); - return (Supplier>) () -> resolveFieldWithInfoFn - .apply(executionContext, callParameters) - .thenCompose(FieldValueInfo::getFieldValue) - .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult)); + return dfCache.computeIfAbsent( + currentField.getName(), + // The same field can be associated with multiple defer executions, so + // we memoize the field resolution to avoid multiple calls to the same data fetcher + key -> FpKit.interThreadMemoize(() -> resolveFieldWithInfoFn + .apply(executionContext, callParameters) + .thenCompose(FieldValueInfo::getFieldValue) + .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult))) + ); + }) .collect(Collectors.toList()); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 6aaf13f097..96f5d76a94 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -221,13 +221,6 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex return result; } - protected CompletableFuture resolveFieldWithInfoToNull(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - FetchedValue fetchedValue = FetchedValue.newFetchedValue().build(); - FieldValueInfo fieldValueInfo = completeField(executionContext, parameters, fetchedValue); - return CompletableFuture.completedFuture(fieldValueInfo); - } - - /** * Called to fetch a value for a field from the {@link DataFetcher} associated with the field * {@link GraphQLFieldDefinition}. diff --git a/src/main/java/graphql/execution/defer/DeferExecutionSupport.java b/src/main/java/graphql/execution/defer/DeferExecutionSupport.java index 504e208b39..567ee7fb01 100644 --- a/src/main/java/graphql/execution/defer/DeferExecutionSupport.java +++ b/src/main/java/graphql/execution/defer/DeferExecutionSupport.java @@ -1,26 +1,18 @@ package graphql.execution.defer; import graphql.Internal; -import graphql.execution.ExecutionContext; -import graphql.execution.MergedField; -import graphql.execution.ValuesResolver; import graphql.execution.reactive.SingleSubscriberPublisher; import graphql.incremental.DelayedIncrementalExecutionResult; -import graphql.language.Directive; -import graphql.language.Field; import graphql.util.LockKit; import org.reactivestreams.Publisher; import java.util.Collection; import java.util.Collections; import java.util.Deque; -import java.util.List; -import java.util.Map; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import static graphql.Directives.DeferDirective; import static graphql.incremental.DelayedIncrementalExecutionResultImpl.newIncrementalExecutionResult; /** @@ -36,26 +28,6 @@ public class DeferExecutionSupport { private final AtomicInteger pendingCalls = new AtomicInteger(); private final LockKit.ReentrantLock publisherLock = new LockKit.ReentrantLock(); - - public boolean checkForDeferDirective(MergedField currentField, ExecutionContext executionContext) { - for (Field field : currentField.getFields()) { - List directives = field.getDirectives(DeferDirective.getName()); - // TODO: How to best deal with repeated directives here - @defer/@stream is not a repeated directive - Directive directive = directives.stream().findFirst().orElse(null); - if (directive != null) { - Map argumentValues = ValuesResolver.getArgumentValues( - DeferDirective.getArguments(), - directive.getArguments(), - executionContext.getCoercedVariables(), - executionContext.getGraphQLContext(), - executionContext.getLocale() - ); - return (Boolean) argumentValues.get("if"); - } - } - return false; - } - @SuppressWarnings("FutureReturnValueIgnored") private void drainDeferredCalls() { DeferredCall deferredCall = deferredCalls.poll(); @@ -68,12 +40,12 @@ private void drainDeferredCalls() { return; } + // The assigment of `remainingCalls` and `publisher.offer` need to be synchronized to ensure + // `hasNext` is `false` precisely on the last event offered to the publisher. publisherLock.lock(); final int remainingCalls; try { - // The assigment of `remainingCalls` and `publisher.offer` need to be synchronized to ensure - // `hasNext` is `false` precisely on the last event offered to the publisher. remainingCalls = pendingCalls.decrementAndGet(); DelayedIncrementalExecutionResult executionResult = newIncrementalExecutionResult() @@ -85,7 +57,6 @@ private void drainDeferredCalls() { } finally { publisherLock.unlock(); } - ; if (remainingCalls == 0) { publisher.noMoreData(); diff --git a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy index cfb2d37cd1..dc0870382d 100644 --- a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy +++ b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy @@ -4,18 +4,11 @@ import graphql.DeferredExecutionResult import graphql.ExecutionResult import graphql.ExecutionResultImpl import graphql.execution.ResultPath -import graphql.language.Argument -import graphql.language.BooleanValue -import graphql.language.Directive -import graphql.language.Field -import graphql.language.VariableReference import org.awaitility.Awaitility import spock.lang.Specification import java.util.concurrent.CompletableFuture -import static graphql.TestUtil.mergedField - class DeferSupportTest extends Specification { @@ -177,74 +170,6 @@ class DeferSupportTest extends Specification { deferPresent2 } - def "detects @defer directive"() { - given: - def deferSupport = new DeferExecutionSupport() - - when: - def noDirectivePresent = deferSupport.checkForDeferDirective(mergedField([ - new Field("a"), - new Field("b") - ]), [:]) - - then: - !noDirectivePresent - - when: - def directivePresent = deferSupport.checkForDeferDirective(mergedField([ - Field.newField("a").directives([new Directive("defer")]).build(), - new Field("b") - ]), [:]) - - then: - directivePresent - } - - def "detects @defer directive can be controlled via if"() { - given: - def deferSupport = new DeferExecutionSupport() - - when: - def ifArg = new Argument("if", new BooleanValue(false)) - def directivePresent = deferSupport.checkForDeferDirective(mergedField([ - Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), - new Field("b") - ]), [:]) - - then: - !directivePresent - - when: - ifArg = new Argument("if", new BooleanValue(true)) - directivePresent = deferSupport.checkForDeferDirective(mergedField([ - Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), - new Field("b") - ]), [:]) - - then: - directivePresent - - when: - ifArg = new Argument("if", new VariableReference("varRef")) - directivePresent = deferSupport.checkForDeferDirective(mergedField([ - Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), - new Field("b") - ]), [varRef: false]) - - then: - !directivePresent - - when: - ifArg = new Argument("if", new VariableReference("varRef")) - directivePresent = deferSupport.checkForDeferDirective(mergedField([ - Field.newField("a").directives([new Directive("defer", [ifArg])]).build(), - new Field("b") - ]), [varRef: true]) - - then: - directivePresent - } - private static DeferredCall offThread(String data, int sleepTime, String path) { def callSupplier = { CompletableFuture.supplyAsync({ diff --git a/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy similarity index 99% rename from src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy rename to src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 3d87e432f4..83128b2788 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -25,7 +25,7 @@ import java.util.concurrent.CompletableFuture import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring -class DeferSupportIntegrationTest extends Specification { +class DeferExecutionSupportIntegrationTest extends Specification { def schemaSpec = ''' type Query { post : Post From ea9726af96f79d7441d1c9976a8eed1571720229 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 19 Jan 2024 17:57:36 +1100 Subject: [PATCH 104/393] Remove code --- .../execution/AsyncExecutionStrategy.java | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index d7493308bd..7fd51ead5d 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -9,10 +9,7 @@ import graphql.execution.incremental.DeferExecution; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import graphql.schema.GraphQLFieldDefinition; import graphql.util.FpKit; import java.util.HashMap; @@ -121,39 +118,6 @@ public CompletableFuture execute(ExecutionContext executionCont return overallResult; } - @SuppressWarnings("FutureReturnValueIgnored") - private Supplier> deferredExecutionResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - return () -> { - GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); - // TODO: freis: This is suddenly not needed anymore -// GraphQLObjectType fieldContainer = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); - - Instrumentation instrumentation = executionContext.getInstrumentation(); - - Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); - - InstrumentationContext fieldCtx = instrumentation.beginDeferredField( - new InstrumentationDeferredFieldParameters(executionContext, executionStepInfo, parameters), - executionContext.getInstrumentationState() - ); - - CompletableFuture result = new CompletableFuture<>(); - fieldCtx.onDispatched(result); - CompletableFuture fieldValueInfoFuture = resolveFieldWithInfo(executionContext, parameters); - - fieldValueInfoFuture.whenComplete((fieldValueInfo, throwable) -> { - // TODO: -// fieldCtx.onFieldValueInfo(fieldValueInfo); - - CompletableFuture execResultFuture = fieldValueInfo.getFieldValue(); - execResultFuture = execResultFuture.whenComplete(fieldCtx::onCompleted); - Async.copyResults(execResultFuture, result); - }); - return result; - }; - } - - private static class SomethingDefer { private final ImmutableListMultimap deferExecutionToFields; private final ImmutableSet deferredFields; From d03c8db6c64038ba05de6990b8cdaff8f5312108 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 23 Jan 2024 17:02:54 +1100 Subject: [PATCH 105/393] Remove unnecessary code and cover more edge cases --- .../java/graphql/DeferredExecutionResult.java | 16 - .../graphql/DeferredExecutionResultImpl.java | 71 --- src/main/java/graphql/GraphQL.java | 7 - .../execution/AsyncExecutionStrategy.java | 229 +++++--- .../java/graphql/execution/Execution.java | 9 +- .../graphql/execution/ExecutionContext.java | 8 +- ...xecutionSupport.java => DeferContext.java} | 2 +- .../graphql/execution/defer/DeferredCall.java | 18 +- .../incremental/IncrementalUtils.java | 15 - .../groovy/example/http/DeferHttpSupport.java | 27 +- src/test/groovy/example/http/HttpMain.java | 12 +- .../execution/defer/BasicSubscriber.groovy | 7 +- .../defer/CapturingSubscriber.groovy | 2 +- .../execution/defer/DeferContextTest.groovy | 212 ++++++++ .../defer/DeferSupportIntegrationTest.groovy | 328 ------------ .../execution/defer/DeferSupportTest.groovy | 197 ------- .../defer/DeferredErrorSupportTest.groovy | 4 +- ...eferExecutionSupportIntegrationTest.groovy | 505 +++++++++++------- .../DataLoaderPerformanceTest.groovy | 9 +- ...manceWithChainedInstrumentationTest.groovy | 9 +- ...Examples.java => IncrementalExamples.java} | 20 +- 21 files changed, 742 insertions(+), 965 deletions(-) delete mode 100644 src/main/java/graphql/DeferredExecutionResult.java delete mode 100644 src/main/java/graphql/DeferredExecutionResultImpl.java rename src/main/java/graphql/execution/defer/{DeferExecutionSupport.java => DeferContext.java} (99%) create mode 100644 src/test/groovy/graphql/execution/defer/DeferContextTest.groovy delete mode 100644 src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy delete mode 100644 src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy rename src/test/groovy/readme/{DeferredExamples.java => IncrementalExamples.java} (76%) diff --git a/src/main/java/graphql/DeferredExecutionResult.java b/src/main/java/graphql/DeferredExecutionResult.java deleted file mode 100644 index 18eb23c74a..0000000000 --- a/src/main/java/graphql/DeferredExecutionResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package graphql; - -import java.util.List; - -/** - * Results that come back from @defer fields have an extra path property that tells you where - * that deferred result came in the original query - */ -@PublicApi -public interface DeferredExecutionResult extends ExecutionResult { - - /** - * @return the execution path of this deferred result in the original query - */ - List getPath(); -} diff --git a/src/main/java/graphql/DeferredExecutionResultImpl.java b/src/main/java/graphql/DeferredExecutionResultImpl.java deleted file mode 100644 index 4aad7e1f85..0000000000 --- a/src/main/java/graphql/DeferredExecutionResultImpl.java +++ /dev/null @@ -1,71 +0,0 @@ -package graphql; - -import graphql.execution.ResultPath; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static graphql.Assert.assertNotNull; - -/** - * Results that come back from @defer fields have an extra path property that tells you where - * that deferred result came in the original query - */ -@PublicApi -public class DeferredExecutionResultImpl extends ExecutionResultImpl implements DeferredExecutionResult { - - private final List path; - - private DeferredExecutionResultImpl(List path, ExecutionResultImpl executionResult) { - super(executionResult); - this.path = assertNotNull(path); - } - - /** - * @return the execution path of this deferred result in the original query - */ - public List getPath() { - return path; - } - - @Override - public Map toSpecification() { - Map map = new LinkedHashMap<>(super.toSpecification()); - map.put("path", path); - return map; - } - - public static Builder newDeferredExecutionResult() { - return new Builder(); - } - - public static class Builder { - private List path = Collections.emptyList(); - private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); - - public Builder path(ResultPath path) { - this.path = assertNotNull(path).toList(); - return this; - } - - public Builder from(ExecutionResult executionResult) { - builder.from((ExecutionResultImpl) executionResult); - return this; - } - - public Builder addErrors(List errors) { - builder.addErrors(errors); - return this; - } - - public DeferredExecutionResult build() { - // TODO: This class will be deleted anyway -// ExecutionResultImpl build = builder.build(); -// return new DeferredExecutionResultImpl(path, build); - - return null; - } - } -} diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index b49902fda2..dbe56f58ed 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -90,13 +90,6 @@ @SuppressWarnings("Duplicates") @PublicApi public class GraphQL { - - /** - * When @defer directives are used, this is the extension key name used to contain the {@link org.reactivestreams.Publisher} - * of deferred results - */ - public static final String DEFERRED_RESULTS = "deferredResults"; - private static final Logger log = LoggerFactory.getLogger(GraphQL.class); private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(GraphQL.class); diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 7fd51ead5d..a324b8f08f 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -1,5 +1,6 @@ package graphql.execution; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import graphql.ExecutionResult; @@ -12,6 +13,7 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.util.FpKit; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -59,18 +61,17 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); -// if(true /* check if incremental support is enabled*/) { - SomethingDefer somethingDefer = new SomethingDefer( + DeferExecutionSupport deferExecutionSupport = executionContext.getExecutionInput().isIncrementalSupport() ? + new DeferExecutionSupport.DeferExecutionSupportImpl( fields, parameters, executionContext, this::resolveFieldWithInfo - ); + ) : DeferExecutionSupport.NOOP; - executionContext.getDeferSupport().enqueue(somethingDefer.createCalls()); -// } + executionContext.getDefferContext().enqueue(deferExecutionSupport.createCalls()); - Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size() - somethingDefer.deferredFields.size()); + Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size() - deferExecutionSupport.deferredFieldsCount()); for (String fieldName : fieldNames) { MergedField currentField = fields.getSubField(fieldName); @@ -81,19 +82,20 @@ public CompletableFuture execute(ExecutionContext executionCont CompletableFuture future; - if (somethingDefer.isDeferredField(currentField)) { + if (deferExecutionSupport.isDeferredField(currentField)) { executionStrategyCtx.onDeferredField(currentField); } else { future = resolveFieldWithInfo(executionContext, newParameters); futures.add(future); } - } CompletableFuture overallResult = new CompletableFuture<>(); executionStrategyCtx.onDispatched(overallResult); futures.await().whenComplete((completeValueInfos, throwable) -> { - BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldNames, overallResult); + List fieldsExecutedInInitialResult = deferExecutionSupport.getNonDeferredFieldNames(fieldNames); + + BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldsExecutedInInitialResult, overallResult); if (throwable != null) { handleResultsConsumer.accept(null, throwable.getCause()); return; @@ -118,88 +120,143 @@ public CompletableFuture execute(ExecutionContext executionCont return overallResult; } - private static class SomethingDefer { - private final ImmutableListMultimap deferExecutionToFields; - private final ImmutableSet deferredFields; - private final ExecutionStrategyParameters parameters; - private final ExecutionContext executionContext; - private final BiFunction> resolveFieldWithInfoFn; - private final Map>> dfCache = new HashMap<>(); - - private SomethingDefer( - MergedSelectionSet mergedSelectionSet, - ExecutionStrategyParameters parameters, - ExecutionContext executionContext, - BiFunction> resolveFieldWithInfoFn - ) { - this.executionContext = executionContext; - this.resolveFieldWithInfoFn = resolveFieldWithInfoFn; - ImmutableListMultimap.Builder deferExecutionToFieldsBuilder = ImmutableListMultimap.builder(); - ImmutableSet.Builder deferredFieldsBuilder = ImmutableSet.builder(); - - mergedSelectionSet.getSubFields().values().forEach(mergedField -> { - mergedField.getDeferExecutions().forEach(de -> { - deferExecutionToFieldsBuilder.put(de, mergedField); - deferredFieldsBuilder.add(mergedField); + private interface DeferExecutionSupport { + + boolean isDeferredField(MergedField mergedField); + + int deferredFieldsCount(); + + List getNonDeferredFieldNames(List allFieldNames); + + Set createCalls(); + + DeferExecutionSupport NOOP = new DeferExecutionSupport.NoOp(); + + class DeferExecutionSupportImpl implements DeferExecutionSupport { + private final ImmutableListMultimap deferExecutionToFields; + private final ImmutableSet deferredFields; + private final ImmutableList nonDeferredFieldNames; + private final ExecutionStrategyParameters parameters; + private final ExecutionContext executionContext; + private final BiFunction> resolveFieldWithInfoFn; + private final Map>> dfCache = new HashMap<>(); + + private DeferExecutionSupportImpl( + MergedSelectionSet mergedSelectionSet, + ExecutionStrategyParameters parameters, + ExecutionContext executionContext, + BiFunction> resolveFieldWithInfoFn + ) { + this.executionContext = executionContext; + this.resolveFieldWithInfoFn = resolveFieldWithInfoFn; + ImmutableListMultimap.Builder deferExecutionToFieldsBuilder = ImmutableListMultimap.builder(); + ImmutableSet.Builder deferredFieldsBuilder = ImmutableSet.builder(); + ImmutableList.Builder nonDeferredFieldNamesBuilder = ImmutableList.builder(); + + mergedSelectionSet.getSubFields().values().forEach(mergedField -> { + mergedField.getDeferExecutions().forEach(de -> { + deferExecutionToFieldsBuilder.put(de, mergedField); + deferredFieldsBuilder.add(mergedField); + }); + + if (mergedField.getDeferExecutions().isEmpty()) { + nonDeferredFieldNamesBuilder.add(mergedField.getSingleField().getResultKey()); + } }); - }); - this.deferExecutionToFields = deferExecutionToFieldsBuilder.build(); - this.deferredFields = deferredFieldsBuilder.build(); - this.parameters = parameters; - } + this.deferExecutionToFields = deferExecutionToFieldsBuilder.build(); + this.deferredFields = deferredFieldsBuilder.build(); + this.parameters = parameters; + this.nonDeferredFieldNames = nonDeferredFieldNamesBuilder.build(); + } - private boolean isDeferredField(MergedField mergedField) { - return deferredFields.contains(mergedField); - } + @Override + public boolean isDeferredField(MergedField mergedField) { + return deferredFields.contains(mergedField); + } - private Set createCalls() { - return deferExecutionToFields.keySet().stream() - .map(deferExecution -> { - DeferredErrorSupport errorSupport = new DeferredErrorSupport(); - - List mergedFields = deferExecutionToFields.get(deferExecution); - - List>> collect = mergedFields.stream() - .map(currentField -> { - Map fields = new LinkedHashMap<>(); - fields.put(currentField.getName(), currentField); - - ExecutionStrategyParameters callParameters = parameters.transform(builder -> - { - MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); - builder.deferredErrorSupport(errorSupport) - .field(currentField) - .fields(mergedSelectionSet) - .path(builder.path.segment(currentField.getName())) - .parent(null); // this is a break in the parent -> child chain - it's a new start effectively - } - ); - - return dfCache.computeIfAbsent( - currentField.getName(), - // The same field can be associated with multiple defer executions, so - // we memoize the field resolution to avoid multiple calls to the same data fetcher - key -> FpKit.interThreadMemoize(() -> resolveFieldWithInfoFn - .apply(executionContext, callParameters) - .thenCompose(FieldValueInfo::getFieldValue) - .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult))) - ); - - - }) - .collect(Collectors.toList()); - - // with a deferred field we are really resetting where we execute from, that is from this current field onwards - return new DeferredCall( - deferExecution.getLabel(), - this.parameters.getPath(), - collect, - errorSupport - ); - }) - .collect(Collectors.toSet()); + @Override + public int deferredFieldsCount() { + return deferredFields.size(); + } + + @Override + public List getNonDeferredFieldNames(List allFieldNames) { + return this.nonDeferredFieldNames; + } + + @Override + public Set createCalls() { + return deferExecutionToFields.keySet().stream() + .map(deferExecution -> { + DeferredErrorSupport errorSupport = new DeferredErrorSupport(); + + List mergedFields = deferExecutionToFields.get(deferExecution); + + List>> collect = mergedFields.stream() + .map(currentField -> { + Map fields = new LinkedHashMap<>(); + fields.put(currentField.getName(), currentField); + + ExecutionStrategyParameters callParameters = parameters.transform(builder -> + { + MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); + builder.deferredErrorSupport(errorSupport) + .field(currentField) + .fields(mergedSelectionSet) + .path(parameters.getPath().segment(currentField.getName())) + .parent(null); // this is a break in the parent -> child chain - it's a new start effectively + } + ); + + return dfCache.computeIfAbsent( + currentField.getName(), + // The same field can be associated with multiple defer executions, so + // we memoize the field resolution to avoid multiple calls to the same data fetcher + key -> FpKit.interThreadMemoize(() -> resolveFieldWithInfoFn + .apply(executionContext, callParameters) + .thenCompose(FieldValueInfo::getFieldValue) + .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult))) + ); + + }) + .collect(Collectors.toList()); + + // with a deferred field we are really resetting where we execute from, that is from this current field onwards + return new DeferredCall( + deferExecution.getLabel(), + this.parameters.getPath(), + collect, + errorSupport + ); + }) + .collect(Collectors.toSet()); + } } + class NoOp implements DeferExecutionSupport { + + @Override + public boolean isDeferredField(MergedField mergedField) { + return false; + } + + @Override + public int deferredFieldsCount() { + return 0; + } + + @Override + public List getNonDeferredFieldNames(List allFieldNames) { + return allFieldNames; + } + + @Override + public Set createCalls() { + return Collections.emptySet(); + } + } } + + } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index ba9894369e..ec5edb2010 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -7,7 +7,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; -import graphql.execution.defer.DeferExecutionSupport; +import graphql.execution.defer.DeferContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -194,7 +194,7 @@ private CompletableFuture executeOperation(ExecutionContext exe */ private CompletableFuture deferSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { - DeferExecutionSupport deferSupport = executionContext.getDeferSupport(); + DeferContext deferSupport = executionContext.getDefferContext(); if (deferSupport.isDeferDetected()) { // we start the rest of the query now to maximize throughput. We have the initial important results // and now we can start the rest of the calls as early as possible (even before some one subscribes) @@ -205,11 +205,6 @@ private CompletableFuture deferSupport(ExecutionContext executi .hasNext(true) .incrementalItemPublisher(publisher) .build(); -// -// -// return ExecutionResultImpl.newExecutionResult().from(er) -// .addExtension(GraphQL.DEFERRED_RESULTS, publisher) -// .build(); } return er; }); diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index b9ca551e51..aaa4cfb50a 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -8,7 +8,7 @@ import graphql.GraphQLError; import graphql.PublicApi; import graphql.collect.ImmutableKit; -import graphql.execution.defer.DeferExecutionSupport; +import graphql.execution.defer.DeferContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; @@ -54,7 +54,7 @@ public class ExecutionContext { private final Set errorPaths = new HashSet<>(); private final DataLoaderRegistry dataLoaderRegistry; private final Locale locale; - private final DeferExecutionSupport deferSupport = new DeferExecutionSupport(); + private final DeferContext defferContext = new DeferContext(); private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; @@ -257,8 +257,8 @@ public ExecutionStrategy getSubscriptionStrategy() { return subscriptionStrategy; } - public DeferExecutionSupport getDeferSupport() { - return deferSupport; + public DeferContext getDefferContext() { + return defferContext; } public ExecutionStrategy getStrategy(OperationDefinition.Operation operation) { diff --git a/src/main/java/graphql/execution/defer/DeferExecutionSupport.java b/src/main/java/graphql/execution/defer/DeferContext.java similarity index 99% rename from src/main/java/graphql/execution/defer/DeferExecutionSupport.java rename to src/main/java/graphql/execution/defer/DeferContext.java index 567ee7fb01..2ee52f2dd7 100644 --- a/src/main/java/graphql/execution/defer/DeferExecutionSupport.java +++ b/src/main/java/graphql/execution/defer/DeferContext.java @@ -21,7 +21,7 @@ */ @Internal // TODO: This should be called IncrementalSupport and handle both @defer and @stream -public class DeferExecutionSupport { +public class DeferContext { private final AtomicBoolean deferDetected = new AtomicBoolean(false); private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index b1e68444c3..72a9166071 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -6,16 +6,12 @@ import graphql.execution.Async; import graphql.execution.ResultPath; import graphql.incremental.DeferPayload; -import graphql.incremental.DelayedIncrementalExecutionResult; -import graphql.incremental.DelayedIncrementalExecutionResultImpl; -import java.util.Collections; -import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; -import java.util.stream.Collectors; /** * This represents a deferred call (aka @defer) to get an execution result sometime after @@ -53,17 +49,17 @@ private DeferPayload transformToDeferredPayload(List f // TODO: Not sure how/if this errorSupport works List errorsEncountered = errorSupport.getErrors(); - Map data = fieldWithExecutionResults.stream() - .collect(Collectors.toMap( - fieldWithExecutionResult -> fieldWithExecutionResult.fieldName, - fieldWithExecutionResult -> fieldWithExecutionResult.executionResult.getData() - )); + Map dataMap = new HashMap<>(); + + fieldWithExecutionResults.forEach(entry -> { + dataMap.put(entry.fieldName, entry.executionResult.getData()); + }); return DeferPayload.newDeferredItem() .errors(errorsEncountered) .path(path) .label(label) - .data(data) + .data(dataMap) .build(); } diff --git a/src/main/java/graphql/execution/incremental/IncrementalUtils.java b/src/main/java/graphql/execution/incremental/IncrementalUtils.java index b107b575e9..8ca269d97a 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalUtils.java +++ b/src/main/java/graphql/execution/incremental/IncrementalUtils.java @@ -1,31 +1,16 @@ package graphql.execution.incremental; import graphql.Assert; -import graphql.DeferredExecutionResult; import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.CoercedVariables; -import graphql.execution.ExecutionContext; -import graphql.execution.MergedField; import graphql.execution.ValuesResolver; -import graphql.execution.defer.DeferredCall; -import graphql.execution.reactive.SingleSubscriberPublisher; import graphql.language.Directive; -import graphql.language.Field; import graphql.language.NodeUtil; -import graphql.language.TypeName; -import graphql.schema.GraphQLObjectType; -import org.reactivestreams.Publisher; -import javax.annotation.Nullable; -import java.util.Deque; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.atomic.AtomicBoolean; import static graphql.Directives.DeferDirective; diff --git a/src/test/groovy/example/http/DeferHttpSupport.java b/src/test/groovy/example/http/DeferHttpSupport.java index a0ef11f625..3f3648449f 100644 --- a/src/test/groovy/example/http/DeferHttpSupport.java +++ b/src/test/groovy/example/http/DeferHttpSupport.java @@ -1,8 +1,7 @@ package example.http; -import graphql.DeferredExecutionResult; -import graphql.ExecutionResult; -import graphql.GraphQL; +import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.IncrementalExecutionResult; import jakarta.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -18,17 +17,23 @@ public class DeferHttpSupport { private static final String CRLF = "\r\n"; @SuppressWarnings("unchecked") - static void sendDeferredResponse(HttpServletResponse response, ExecutionResult executionResult, Map extensions) { - Publisher deferredResults = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS); + static void sendIncrementalResponse(HttpServletResponse response, IncrementalExecutionResult incrementalExecutionResult) { + + Publisher incrementalResults = incrementalExecutionResult.getIncrementalItemPublisher(); + try { - sendMultipartResponse(response, executionResult, deferredResults); + sendMultipartResponse(response, incrementalExecutionResult, incrementalResults); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } - static private void sendMultipartResponse(HttpServletResponse response, ExecutionResult executionResult, Publisher deferredResults) { + static private void sendMultipartResponse( + HttpServletResponse response, + IncrementalExecutionResult incrementalExecutionResult, + Publisher incrementalResults + ) { // this implements this apollo defer spec: https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md // the spec says CRLF + "-----" + CRLF is needed at the end, but it works without it and with it we get client // side errors with it, so we skp it @@ -38,11 +43,11 @@ static private void sendMultipartResponse(HttpServletResponse response, Executio response.setHeader("Connection", "keep-alive"); // send the first "un deferred" part of the result - writeAndFlushPart(response, executionResult.toSpecification()); + writeAndFlushPart(response, incrementalExecutionResult.toSpecification()); // now send each deferred part which is given to us as a reactive stream // of deferred values - deferredResults.subscribe(new Subscriber() { + incrementalResults.subscribe(new Subscriber() { Subscription subscription; @Override @@ -52,10 +57,10 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(DeferredExecutionResult deferredExecutionResult) { + public void onNext(DelayedIncrementalExecutionResult delayedIncrementalExecutionResult) { subscription.request(1); - writeAndFlushPart(response, deferredExecutionResult.toSpecification()); + writeAndFlushPart(response, delayedIncrementalExecutionResult.toSpecification()); } @Override diff --git a/src/test/groovy/example/http/HttpMain.java b/src/test/groovy/example/http/HttpMain.java index 0b84f96e72..86507f33fa 100644 --- a/src/test/groovy/example/http/HttpMain.java +++ b/src/test/groovy/example/http/HttpMain.java @@ -8,6 +8,7 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; import graphql.execution.instrumentation.tracing.TracingInstrumentation; +import graphql.incremental.IncrementalExecutionResult; import graphql.schema.DataFetcher; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -16,6 +17,9 @@ import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.dataloader.BatchLoader; import org.dataloader.DataLoader; import org.dataloader.DataLoaderFactory; @@ -27,9 +31,6 @@ import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.ResourceHandler; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -160,9 +161,8 @@ private void handleStarWars(HttpServletRequest httpRequest, HttpServletResponse private void returnAsJson(HttpServletResponse response, ExecutionResult executionResult) throws IOException { - Map extensions = executionResult.getExtensions(); - if (extensions != null && extensions.containsKey(GraphQL.DEFERRED_RESULTS)) { - DeferHttpSupport.sendDeferredResponse(response, executionResult, extensions); + if (executionResult instanceof IncrementalExecutionResult) { + DeferHttpSupport.sendIncrementalResponse(response, (IncrementalExecutionResult) executionResult); } sendNormalResponse(response, executionResult); } diff --git a/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy b/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy index e1f9be0cc5..564a41e2b6 100644 --- a/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy +++ b/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy @@ -1,13 +1,12 @@ package graphql.execution.defer -import graphql.DeferredExecutionResult -import graphql.ExecutionResult +import graphql.incremental.DelayedIncrementalExecutionResult import org.reactivestreams.Subscriber import org.reactivestreams.Subscription import java.util.concurrent.atomic.AtomicBoolean -class BasicSubscriber implements Subscriber { +class BasicSubscriber implements Subscriber { Subscription subscription AtomicBoolean finished = new AtomicBoolean() Throwable throwable @@ -20,7 +19,7 @@ class BasicSubscriber implements Subscriber { } @Override - void onNext(DeferredExecutionResult executionResult) { + void onNext(DelayedIncrementalExecutionResult executionResult) { } @Override diff --git a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy index 8401a3cf1e..46d0b63046 100644 --- a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy +++ b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy @@ -1,6 +1,6 @@ package graphql.execution.defer -import graphql.DeferredExecutionResult + import graphql.incremental.DeferPayload import graphql.incremental.DelayedIncrementalExecutionResult import org.reactivestreams.Publisher diff --git a/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy b/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy new file mode 100644 index 0000000000..31a0ac3eea --- /dev/null +++ b/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy @@ -0,0 +1,212 @@ +package graphql.execution.defer + +import graphql.ExecutionResult +import graphql.ExecutionResultImpl +import graphql.execution.ResultPath +import graphql.incremental.DelayedIncrementalExecutionResult +import org.awaitility.Awaitility +import spock.lang.Specification + +import java.util.concurrent.CompletableFuture +import java.util.function.Supplier + +class DeferContextTest extends Specification { + + + def "emits N deferred calls - ordering depends on call latency"() { + + given: + def deferContext = new DeferContext() + deferContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + deferContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + + when: + List results = [] + def subscriber = new BasicSubscriber() { + @Override + void onNext(DelayedIncrementalExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + } + deferContext.startDeferredCalls().subscribe(subscriber) + Awaitility.await().untilTrue(subscriber.finished) + then: + + results.size() == 3 + results[0].incrementalItems[0].data["c"] == "C" + results[1].incrementalItems[0].data["b"] == "B" + results[2].incrementalItems[0].data["a"] == "A" + } + + def "calls within calls are enqueued correctly"() { + given: + def deferContext = new DeferContext() + deferContext.enqueue(offThreadCallWithinCall(deferContext, "A", "A_Child", 500, "/a")) + deferContext.enqueue(offThreadCallWithinCall(deferContext, "B", "B_Child", 300, "/b")) + deferContext.enqueue(offThreadCallWithinCall(deferContext, "C", "C_Child", 100, "/c")) + + when: + List results = [] + BasicSubscriber subscriber = new BasicSubscriber() { + @Override + void onNext(DelayedIncrementalExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + } + deferContext.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + then: + + results.size() == 6 + results[0].incrementalItems[0].data["c"] == "C" + results[1].incrementalItems[0].data["c_child"] == "C_Child" + results[2].incrementalItems[0].data["b"] == "B" + results[3].incrementalItems[0].data["a"] == "A" + results[4].incrementalItems[0].data["b_child"] == "B_Child" + results[5].incrementalItems[0].data["a_child"] == "A_Child" + } + + def "stops at first exception encountered"() { + given: + def deferContext = new DeferContext() + deferContext.enqueue(offThread("A", 100, "/field/path")) + deferContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception + deferContext.enqueue(offThread("C", 10, "/field/path")) + + when: + List results = [] + Throwable thrown = null + def subscriber = new BasicSubscriber() { + @Override + void onNext(DelayedIncrementalExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + + @Override + void onError(Throwable t) { + thrown = t + finished.set(true) + } + + @Override + void onComplete() { + assert false, "This should not be called!" + } + } + deferContext.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + then: + + thrown.message == "java.lang.RuntimeException: Bang" + results[0].incrementalItems[0].data["c"] == "C" + } + + def "you can cancel the subscription"() { + given: + def deferContext = new DeferContext() + deferContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + deferContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + + when: + List results = [] + def subscriber = new BasicSubscriber() { + @Override + void onNext(DelayedIncrementalExecutionResult executionResult) { + results.add(executionResult) + subscription.cancel() + finished.set(true) + } + } + deferContext.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + then: + + results.size() == 1 + results[0].incrementalItems[0].data["c"] == "C" + } + + def "you cant subscribe twice"() { + given: + def deferContext = new DeferContext() + deferContext.enqueue(offThread("A", 100, "/field/path")) + deferContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second + deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + + when: + Throwable expectedThrowable + deferContext.startDeferredCalls().subscribe(new BasicSubscriber()) + deferContext.startDeferredCalls().subscribe(new BasicSubscriber() { + @Override + void onError(Throwable t) { + expectedThrowable = t + } + }) + then: + expectedThrowable != null + } + + def "indicates if there are any defers present"() { + given: + def deferContext = new DeferContext() + + when: + def deferPresent1 = deferContext.isDeferDetected() + + then: + !deferPresent1 + + when: + deferContext.enqueue(offThread("A", 100, "/field/path")) + def deferPresent2 = deferContext.isDeferDetected() + + then: + deferPresent2 + } + + def "multiple fields are part of the same call"() { + + } + + def "race condition"() { + + } + + private static DeferredCall offThread(String data, int sleepTime, String path) { + def callSupplier = new Supplier>() { + @Override + CompletableFuture get() { + return CompletableFuture.supplyAsync({ + Thread.sleep(sleepTime) + if (data == "Bang") { + throw new RuntimeException(data) + } + new DeferredCall.FieldWithExecutionResult(data.toLowerCase(), new ExecutionResultImpl(data, [])) + }) + } + } + + return new DeferredCall(null, ResultPath.parse(path), [callSupplier], new DeferredErrorSupport()) + } + + private static DeferredCall offThreadCallWithinCall(DeferContext deferContext, String dataParent, String dataChild, int sleepTime, String path) { + def callSupplier = new Supplier>() { + @Override + CompletableFuture get() { + CompletableFuture.supplyAsync({ + Thread.sleep(sleepTime) + deferContext.enqueue(offThread(dataChild, sleepTime, path)) + new DeferredCall.FieldWithExecutionResult(dataParent.toLowerCase(), new ExecutionResultImpl(dataParent, [])) + }) + } + } + return new DeferredCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredErrorSupport()) + } +} diff --git a/src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy deleted file mode 100644 index 6a1a6b8d70..0000000000 --- a/src/test/groovy/graphql/execution/defer/DeferSupportIntegrationTest.groovy +++ /dev/null @@ -1,328 +0,0 @@ -package graphql.execution.defer - -import graphql.DeferredExecutionResult -import graphql.ErrorType -import graphql.Directives -import graphql.ExecutionInput -import graphql.ExecutionResult -import graphql.GraphQL -import graphql.TestUtil -import graphql.schema.DataFetcher -import graphql.schema.DataFetchingEnvironment -import graphql.schema.idl.RuntimeWiring -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorType -import org.awaitility.Awaitility -import org.reactivestreams.Publisher -import spock.lang.Specification - -import java.time.Duration -import java.util.concurrent.CompletableFuture - -import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring - -class DeferSupportIntegrationTest extends Specification { - def then = 0 - - def sentAt() { - def seconds = Duration.ofMillis(System.currentTimeMillis() - then).toMillis() - "T+" + seconds - } - - def sleepSome(DataFetchingEnvironment env) { - Integer sleepTime = env.getArgument("sleepTime") - sleepTime = Optional.ofNullable(sleepTime).orElse(0) - Thread.sleep(sleepTime) - } - - def schemaSpec = ''' - type Query { - post : Post - mandatoryReviews : [Review]! - } - - type Mutation { - mutate(arg : String) : String - } - - type Post { - postText : String - sentAt : String - echo(text : String = "echo") : String - comments(sleepTime : Int, prefix :String) : [Comment] - reviews(sleepTime : Int) : [Review] - } - - type Comment { - commentText : String - sentAt : String - comments(sleepTime : Int, prefix :String) : [Comment] - goes : Bang - } - - type Review { - reviewText : String - sentAt : String - comments(sleepTime : Int, prefix :String) : [Comment] - goes : Bang - } - - type Bang { - bang : String - } - ''' - - DataFetcher postFetcher = new DataFetcher() { - @Override - Object get(DataFetchingEnvironment environment) { - return CompletableFuture.supplyAsync({ - [postText: "post_data", sentAt: sentAt()] - }) - } - } - DataFetcher commentsFetcher = new DataFetcher() { - @Override - Object get(DataFetchingEnvironment env) { - return CompletableFuture.supplyAsync({ - sleepSome(env) - - def prefix = env.getArgument("prefix") - prefix = prefix == null ? "" : prefix - - def result = [] - for (int i = 0; i < 3; i++) { - result.add([commentText: prefix + "comment" + i, sentAt: sentAt(), goes: "goes"]) - } - return result - }) - } - - } - DataFetcher reviewsFetcher = new DataFetcher() { - @Override - Object get(DataFetchingEnvironment env) { - return CompletableFuture.supplyAsync({ - sleepSome(env) - def result = [] - for (int i = 0; i < 3; i++) { - result.add([reviewText: "review" + i, sentAt: sentAt(), goes: "goes"]) - } - return result - }) - } - } - - DataFetcher bangDataFetcher = new DataFetcher() { - @Override - Object get(DataFetchingEnvironment environment) { - throw new RuntimeException("Bang!") - } - } - DataFetcher echoDataFetcher = new DataFetcher() { - @Override - Object get(DataFetchingEnvironment environment) { - return environment.getArgument("text") - } - } - - GraphQL graphQL = null - - void setup() { - then = System.currentTimeMillis() - - def runtimeWiring = RuntimeWiring.newRuntimeWiring() - .type(newTypeWiring("Query").dataFetcher("post", postFetcher)) - .type(newTypeWiring("Post").dataFetcher("comments", commentsFetcher)) - .type(newTypeWiring("Post").dataFetcher("echo", echoDataFetcher)) - .type(newTypeWiring("Post").dataFetcher("reviews", reviewsFetcher)) - .type(newTypeWiring("Bang").dataFetcher("bang", bangDataFetcher)) - - .type(newTypeWiring("Comment").dataFetcher("comments", commentsFetcher)) - .type(newTypeWiring("Review").dataFetcher("comments", commentsFetcher)) - .build() - - def schema = TestUtil.schema(schemaSpec, runtimeWiring) - .transform({ builder -> builder.additionalDirective(Directives.DeferDirective) }) - this.graphQL = GraphQL.newGraphQL(schema).build() - } - - def "test defer support end to end"() { - - def query = ''' - query { - post { - postText - - a :comments(sleepTime:200) @defer { - commentText - } - - b : reviews(sleepTime:100) @defer { - reviewText - comments(prefix : "b_") @defer { - commentText - } - } - - c: reviews @defer { - goes { - bang - } - } - } - } - ''' - - when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) - - then: - initialResult.errors.isEmpty() - initialResult.data == ["post": ["postText": "post_data", a: null, b: null, c: null]] - - when: - - Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher - - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream) - Awaitility.await().untilTrue(subscriber.finished) - - List resultList = subscriber.executionResults - - then: - - assertDeferredData(resultList) - } - - def "test defer support keeps the fields named correctly when interspersed in the query"() { - - def query = ''' - query { - post { - interspersedA: echo(text:"before a:") - - a: comments(sleepTime:200) @defer { - commentText - } - - interspersedB: echo(text:"before b:") - - b : reviews(sleepTime:100) @defer { - reviewText - comments(prefix : "b_") @defer { - commentText - } - } - - interspersedC: echo(text:"before c:") - - c: reviews @defer { - goes { - bang - } - } - - interspersedD: echo(text:"after c:") - } - } - ''' - - when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) - - then: - initialResult.errors.isEmpty() - initialResult.data == ["post": [ - "interspersedA": "before a:", - "a" : null, - "interspersedB": "before b:", - "b" : null, - "interspersedC": "before c:", - "c" : null, - "interspersedD": "after c:", - ]] - - when: - - Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher - - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream); - Awaitility.await().untilTrue(subscriber.finished) - - List resultList = subscriber.executionResults - - then: - - assertDeferredData(resultList) - } - - def assertDeferredData(ArrayList resultList) { - resultList.size() == 6 - - assert resultList[0].data == [[commentText: "comment0"], [commentText: "comment1"], [commentText: "comment2"]] - assert resultList[0].errors == [] - assert resultList[0].path == ["post", "a"] - - assert resultList[1].data == [[reviewText: "review0", comments: null], [reviewText: "review1", comments: null], [reviewText: "review2", comments: null]] - assert resultList[1].errors == [] - assert resultList[1].path == ["post", "b"] - - // exceptions in here - assert resultList[2].errors.size() == 3 - assert resultList[2].errors[0].getMessage() == "Exception while fetching data (/post/c[0]/goes/bang) : Bang!" - assert resultList[2].errors[1].getMessage() == "Exception while fetching data (/post/c[1]/goes/bang) : Bang!" - assert resultList[2].errors[2].getMessage() == "Exception while fetching data (/post/c[2]/goes/bang) : Bang!" - assert resultList[2].path == ["post", "c"] - - // sub defers are sent in encountered order - assert resultList[3].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] - assert resultList[3].errors == [] - assert resultList[3].path == ["post", "b", 0, "comments"] - - assert resultList[4].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] - assert resultList[4].errors == [] - assert resultList[4].path == ["post", "b", 1, "comments"] - - assert resultList[5].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] - assert resultList[5].errors == [] - assert resultList[5].path == ["post", "b", 2, "comments"] - - true - } - - def "nonNull types are not allowed"() { - - def query = ''' - { - mandatoryReviews @defer # nulls are not allowed - { - reviewText - } - } - ''' - when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) - then: - initialResult.errors.size() == 1 - initialResult.errors[0].errorType == ErrorType.ValidationError - (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveOnNonNullField - - } - - def "mutations cant have defers"() { - - def query = ''' - mutation { - mutate(arg : "go") @defer - } - ''' - when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) - then: - initialResult.errors.size() == 1 - initialResult.errors[0].errorType == ErrorType.ValidationError - (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveNotOnQueryOperation - } -} diff --git a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy b/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy deleted file mode 100644 index dc0870382d..0000000000 --- a/src/test/groovy/graphql/execution/defer/DeferSupportTest.groovy +++ /dev/null @@ -1,197 +0,0 @@ -package graphql.execution.defer - -import graphql.DeferredExecutionResult -import graphql.ExecutionResult -import graphql.ExecutionResultImpl -import graphql.execution.ResultPath -import org.awaitility.Awaitility -import spock.lang.Specification - -import java.util.concurrent.CompletableFuture - -class DeferSupportTest extends Specification { - - - def "emits N deferred calls with order preserved"() { - - given: - def deferSupport = new DeferExecutionSupport() - deferSupport.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last - deferSupport.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second - deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first - - when: - List results = [] - def subscriber = new BasicSubscriber() { - @Override - void onNext(DeferredExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - } - deferSupport.startDeferredCalls().subscribe(subscriber) - Awaitility.await().untilTrue(subscriber.finished) - then: - - results.size() == 3 - results[0].data == "A" - results[1].data == "B" - results[2].data == "C" - } - - def "calls within calls are enqueued correctly"() { - given: - def deferSupport = new DeferExecutionSupport() - deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "A", "a", 100, "/a")) - deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "B", "b", 50, "/b")) - deferSupport.enqueue(offThreadCallWithinCall(deferSupport, "C", "c", 10, "/c")) - - when: - List results = [] - BasicSubscriber subscriber = new BasicSubscriber() { - @Override - void onNext(DeferredExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - } - deferSupport.startDeferredCalls().subscribe(subscriber) - - Awaitility.await().untilTrue(subscriber.finished) - then: - - results.size() == 6 - results[0].data == "A" - results[1].data == "B" - results[2].data == "C" - results[3].data == "a" - results[4].data == "b" - results[5].data == "c" - } - - def "stops at first exception encountered"() { - given: - def deferSupport = new DeferExecutionSupport() - deferSupport.enqueue(offThread("A", 100, "/field/path")) - deferSupport.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception - deferSupport.enqueue(offThread("C", 10, "/field/path")) - - when: - List results = [] - Throwable thrown = null - def subscriber = new BasicSubscriber() { - @Override - void onNext(DeferredExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - - @Override - void onError(Throwable t) { - thrown = t - finished.set(true) - } - - @Override - void onComplete() { - assert false, "This should not be called!" - } - } - deferSupport.startDeferredCalls().subscribe(subscriber) - - Awaitility.await().untilTrue(subscriber.finished) - then: - - thrown.message == "java.lang.RuntimeException: Bang" - } - - def "you can cancel the subscription"() { - given: - def deferSupport = new DeferExecutionSupport() - deferSupport.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last - deferSupport.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second - deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first - - when: - List results = [] - def subscriber = new BasicSubscriber() { - @Override - void onNext(DeferredExecutionResult executionResult) { - results.add(executionResult) - subscription.cancel() - finished.set(true) - } - } - deferSupport.startDeferredCalls().subscribe(subscriber) - - Awaitility.await().untilTrue(subscriber.finished) - then: - - results.size() == 1 - results[0].data == "A" - - } - - def "you cant subscribe twice"() { - given: - def deferSupport = new DeferExecutionSupport() - deferSupport.enqueue(offThread("A", 100, "/field/path")) - deferSupport.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second - deferSupport.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first - - when: - Throwable expectedThrowble - deferSupport.startDeferredCalls().subscribe(new BasicSubscriber()) - deferSupport.startDeferredCalls().subscribe(new BasicSubscriber() { - @Override - void onError(Throwable t) { - expectedThrowble = t - } - }) - then: - expectedThrowble != null - } - - def "indicates of there any defers present"() { - given: - def deferSupport = new DeferExecutionSupport() - - when: - def deferPresent1 = deferSupport.isDeferDetected() - - then: - !deferPresent1 - - when: - deferSupport.enqueue(offThread("A", 100, "/field/path")) - def deferPresent2 = deferSupport.isDeferDetected() - - then: - deferPresent2 - } - - private static DeferredCall offThread(String data, int sleepTime, String path) { - def callSupplier = { - CompletableFuture.supplyAsync({ - Thread.sleep(sleepTime) - if (data == "Bang") { - throw new RuntimeException(data) - } - new ExecutionResultImpl(data, []) - }) - } - return new DeferredCall(ResultPath.parse(path), callSupplier, new DeferredErrorSupport()) - } - - private - static DeferredCall offThreadCallWithinCall(DeferExecutionSupport deferSupport, String dataParent, String dataChild, int sleepTime, String path) { - def callSupplier = { - CompletableFuture.supplyAsync({ - Thread.sleep(sleepTime) - deferSupport.enqueue(offThread(dataChild, sleepTime, path)) - new ExecutionResultImpl(dataParent, []) - }) - } - return new DeferredCall(ResultPath.parse("/field/path"), callSupplier, new DeferredErrorSupport()) - } -} diff --git a/src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy b/src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy index 3fc8e8e1a8..40d0c6b747 100644 --- a/src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy +++ b/src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy @@ -1,9 +1,9 @@ package graphql.execution.defer -import graphql.DeferredExecutionResult import graphql.Directives import graphql.ExecutionResult import graphql.GraphQL +import graphql.incremental.DelayedIncrementalExecutionResult import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import org.reactivestreams.Publisher @@ -59,7 +59,7 @@ class DeferredErrorSupportTest extends Specification { def executionResultDeferred = null def subscriber = new BasicSubscriber() { @Override - void onNext(DeferredExecutionResult executionResultStreamed) { + void onNext(DelayedIncrementalExecutionResult executionResultStreamed) { executionResultDeferred = executionResultStreamed subscription.request(1) } diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 83128b2788..405df81c1e 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -1,8 +1,6 @@ package graphql.execution.incremental -import graphql.DeferredExecutionResult import graphql.Directives -import graphql.ErrorType import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQL @@ -13,9 +11,8 @@ import graphql.incremental.IncrementalExecutionResult import graphql.incremental.IncrementalExecutionResultImpl import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment +import graphql.schema.TypeResolver import graphql.schema.idl.RuntimeWiring -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorType import org.awaitility.Awaitility import org.reactivestreams.Publisher import spock.lang.Specification @@ -30,14 +27,29 @@ class DeferExecutionSupportIntegrationTest extends Specification { type Query { post : Post hello: String + item(type: String!): Item } - type Post { + interface Item { + id: ID! + summary: String + text: String + } + + type Page implements Item { + id: ID! + summary: String + text: String + views: Int + } + + type Post implements Item { id: ID! summary : String text : String latestComment: Comment comments: [Comment] + resolvesToNull: String } type Comment { @@ -50,7 +62,10 @@ class DeferExecutionSupportIntegrationTest extends Specification { name: String avatar: String } - + + type Mutation { + addPost: Post + } ''' GraphQL graphQL = null @@ -76,18 +91,42 @@ class DeferExecutionSupportIntegrationTest extends Specification { } } + private static DataFetcher resolveItem() { + return (env) -> { + def data = env.getArgument("type") == "Post" + ? [__typename: "Post", id: "1001"] + : [__typename: "Page", id: "1002"] + + return CompletableFuture.supplyAsync { data } + } + } + + private static TypeResolver itemTypeResolver() { + return (env) -> { + env.getSchema().getObjectType(env.object["__typename"]) + } + } + void setup() { def runtimeWiring = RuntimeWiring.newRuntimeWiring() .type(newTypeWiring("Query") .dataFetcher("post", resolve([id: "1001"])) .dataFetcher("hello", resolve("world")) + .dataFetcher("item", resolveItem()) ) .type(newTypeWiring("Post").dataFetcher("summary", resolve("A summary", 10))) .type(newTypeWiring("Post").dataFetcher("text", resolve("The full text", 100))) .type(newTypeWiring("Post").dataFetcher("latestComment", resolve([title: "Comment title"], 10))) + .type(newTypeWiring("Page").dataFetcher("summary", resolve("A page summary", 10))) + .type(newTypeWiring("Page").dataFetcher("text", resolve("The page full text", 100))) .type(newTypeWiring("Comment").dataFetcher("content", resolve("Full content", 100))) .type(newTypeWiring("Comment").dataFetcher("author", resolve([name: "Author name"], 10))) .type(newTypeWiring("Person").dataFetcher("avatar", resolve("Avatar image", 100))) + .type(newTypeWiring("Mutation") + .dataFetcher("addPost", resolve([id: "1001"])) + ) + .type(newTypeWiring("Item") + .typeResolver(itemTypeResolver())) .build() def schema = TestUtil.schema(schemaSpec, runtimeWiring) @@ -133,6 +172,110 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "defer on interface field"() { + def query = """ + query { + item(type: "$type") { + __typename + id + ... on Item @defer { + summary + } + + ... on Post { + text + } + + ... on Page @defer { + text + } + } + } + """ + + when: + def initialResult = executeQuery(query) + + then: + if (type == "Post") { + assert initialResult.toSpecification() == [ + data : [item: [__typename: "Post", id: "1001", text: "The full text"]], + hasNext: true + ] + } else { + assert initialResult.toSpecification() == [ + data : [item: [__typename: "Page", id: "1002"]], + hasNext: true + ] + } + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + if(type == "Post") { + assert incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["item"], + data: [summary: "A summary"] + ] + ] + ] + ] + } else { + assert incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path: ["item"], + data: [summary: "A page summary"] + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path: ["item"], + data: [text: "The page full text"] + ] + ] + ] + ] + } + + where: + type << ["Page", "Post"] + } + + def "defer execution is ignored if support for incremental delivery is disabled"() { + def query = ''' + query { + post { + id + ... @defer { + summary + } + } + } + ''' + + when: + ExecutionResult initialResult = executeQuery(query, false, [:]) + + then: + !(initialResult instanceof IncrementalExecutionResult) + + initialResult.toSpecification() == [ + data: [post: [id: "1001", summary: "A summary"]], + ] + + } + def "simple defer with label"() { def query = ''' query { @@ -172,6 +315,44 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "deferred field results in 'null'"() { + def query = ''' + query { + post { + id + ... @defer { + resolvesToNull + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + data : [resolvesToNull: null] + ] + ] + ] + ] + } + def "simple defer with fragment definition"() { def query = ''' query { @@ -378,7 +559,6 @@ class DeferExecutionSupportIntegrationTest extends Specification { def incrementalResults = getIncrementalResults(initialResult) then: - incrementalResults == [ [ hasNext : true, @@ -403,6 +583,109 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "order of @defer definition in query doesn't affect order of incremental payloads in response"() { + def query = ''' + query { + post { + id + # "text" is defined before "summary" in the query, but it's slower, so it will be delivered after. + ... @defer { + text + } + ... @defer { + summary + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary"] + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [text: "The full text"] + ] + ] + ] + ] + } + + def "keeps the fields named correctly when interspersed in the query"() { + def query = ''' + query { + post { + firstId: id + ... @defer { + text + } + secondId: id + ... @defer { + summary + } + thirdId: id + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [firstId: "1001", secondId: "1001", thirdId: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary"] + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [text: "The full text"] + ] + ] + ] + ] + } + def "defer result in initial result being empty object"() { def query = ''' query { @@ -562,7 +845,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { when: - def initialResult = executeQuery(query) + IncrementalExecutionResult initialResult = executeQuery(query) then: initialResult.toSpecification() == [ @@ -592,192 +875,56 @@ class DeferExecutionSupportIntegrationTest extends Specification { incrementalResults.any { it.incremental[0].label == "defer-outer" } } - def "test defer support end to end"() { - + def "mutations can have defers"() { def query = ''' - query { - post { - postText - - ... @defer { - a :comments(sleepTime:200) { - commentText - } - } - - ... @defer { - b : reviews(sleepTime:100) { - reviewText - ... @defer { - comments(prefix : "b_") { - commentText - } - } - } - } - + mutation { + addPost { + firstId: id ... @defer { - c: reviews { - goes { - bang - } - } - } - } - } - ''' - - when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) - - then: - initialResult.errors.isEmpty() - initialResult.data == ["post": ["postText": "post_data", a: null, b: null, c: null]] - - when: - - Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher - - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream) - Awaitility.await().untilTrue(subscriber.finished) - - List resultList = subscriber.executionResults - - then: - - assertDeferredData(resultList) - } - - def "test defer support keeps the fields named correctly when interspersed in the query"() { - - def query = ''' - query { - post { - interspersedA: echo(text:"before a:") - - a: comments(sleepTime:200) @defer { - commentText - } - - interspersedB: echo(text:"before b:") - - b : reviews(sleepTime:100) @defer { - reviewText - comments(prefix : "b_") @defer { - commentText - } + text } - - interspersedC: echo(text:"before c:") - - c: reviews @defer { - goes { - bang - } + secondId: id + ... @defer { + summary } - - interspersedD: echo(text:"after c:") + thirdId: id } } ''' when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) - - then: - initialResult.errors.isEmpty() - initialResult.data == ["post": [ - "interspersedA": "before a:", - "a" : null, - "interspersedB": "before b:", - "b" : null, - "interspersedC": "before c:", - "c" : null, - "interspersedD": "after c:", - ]] - - when: - - Publisher deferredResultStream = initialResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher - - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream); - Awaitility.await().untilTrue(subscriber.finished) - - List resultList = subscriber.executionResults + IncrementalExecutionResult initialResult = executeQuery(query) then: + initialResult.toSpecification() == [ + data : [addPost: [firstId: "1001", secondId: "1001", thirdId: "1001"]], + hasNext: true + ] - assertDeferredData(resultList) - } - - def assertDeferredData(ArrayList resultList) { - resultList.size() == 6 - - assert resultList[0].data == [[commentText: "comment0"], [commentText: "comment1"], [commentText: "comment2"]] - assert resultList[0].errors == [] - assert resultList[0].path == ["post", "a"] - - assert resultList[1].data == [[reviewText: "review0", comments: null], [reviewText: "review1", comments: null], [reviewText: "review2", comments: null]] - assert resultList[1].errors == [] - assert resultList[1].path == ["post", "b"] - - // exceptions in here - assert resultList[2].errors.size() == 3 - assert resultList[2].errors[0].getMessage() == "Exception while fetching data (/post/c[0]/goes/bang) : Bang!" - assert resultList[2].errors[1].getMessage() == "Exception while fetching data (/post/c[1]/goes/bang) : Bang!" - assert resultList[2].errors[2].getMessage() == "Exception while fetching data (/post/c[2]/goes/bang) : Bang!" - assert resultList[2].path == ["post", "c"] - - // sub defers are sent in encountered order - assert resultList[3].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] - assert resultList[3].errors == [] - assert resultList[3].path == ["post", "b", 0, "comments"] - - assert resultList[4].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] - assert resultList[4].errors == [] - assert resultList[4].path == ["post", "b", 1, "comments"] - - assert resultList[5].data == [[commentText: "b_comment0"], [commentText: "b_comment1"], [commentText: "b_comment2"]] - assert resultList[5].errors == [] - assert resultList[5].path == ["post", "b", 2, "comments"] - - true - } - - def "nonNull types are not allowed"() { - - def query = ''' - { - mandatoryReviews @defer # nulls are not allowed - { - reviewText - } - } - ''' when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) - then: - initialResult.errors.size() == 1 - initialResult.errors[0].errorType == ErrorType.ValidationError - (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveOnNonNullField - - } - - def "mutations cant have defers"() { + def incrementalResults = getIncrementalResults(initialResult) - def query = ''' - mutation { - mutate(arg : "go") @defer - } - ''' - when: - def initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(query).build()) then: - initialResult.errors.size() == 1 - initialResult.errors[0].errorType == ErrorType.ValidationError - (initialResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferDirectiveNotOnQueryOperation + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path: ["addPost"], + data: [summary: "A summary"] + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path: ["addPost"], + data: [text: "The full text"] + ] + ] + ] + ] } private ExecutionResult executeQuery(String query) { diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index 0df6c7a5ca..d62732eb81 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -1,10 +1,11 @@ package graphql.execution.instrumentation.dataloader -import graphql.DeferredExecutionResult import graphql.ExecutionInput import graphql.GraphQL import graphql.execution.defer.CapturingSubscriber import graphql.execution.instrumentation.Instrumentation +import graphql.incremental.DelayedIncrementalExecutionResult +import graphql.incremental.IncrementalExecutionResult import org.awaitility.Awaitility import org.dataloader.DataLoaderRegistry import org.reactivestreams.Publisher @@ -103,8 +104,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(deferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() def result = graphQL.execute(executionInput) - Map extensions = result.getExtensions() - Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher def subscriber = new CapturingSubscriber() subscriber.subscribeTo(deferredResultStream) @@ -130,8 +130,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveDeferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() def result = graphQL.execute(executionInput) - Map extensions = result.getExtensions() - Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher def subscriber = new CapturingSubscriber() subscriber.subscribeTo(deferredResultStream) diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 7d15890a82..dae4965238 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -1,11 +1,12 @@ package graphql.execution.instrumentation.dataloader -import graphql.DeferredExecutionResult import graphql.ExecutionInput import graphql.GraphQL import graphql.execution.defer.CapturingSubscriber import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation +import graphql.incremental.DelayedIncrementalExecutionResult +import graphql.incremental.IncrementalExecutionResult import org.awaitility.Awaitility import org.dataloader.DataLoaderRegistry import org.reactivestreams.Publisher @@ -112,8 +113,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(deferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() def result = graphQL.execute(executionInput) - Map extensions = result.getExtensions() - Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher def subscriber = new CapturingSubscriber() subscriber.subscribeTo(deferredResultStream) @@ -139,8 +139,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveDeferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() def result = graphQL.execute(executionInput) - Map extensions = result.getExtensions() - Publisher deferredResultStream = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS) + Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher def subscriber = new CapturingSubscriber() subscriber.subscribeTo(deferredResultStream) diff --git a/src/test/groovy/readme/DeferredExamples.java b/src/test/groovy/readme/IncrementalExamples.java similarity index 76% rename from src/test/groovy/readme/DeferredExamples.java rename to src/test/groovy/readme/IncrementalExamples.java index 3d8d7e455e..e53e8cc1eb 100644 --- a/src/test/groovy/readme/DeferredExamples.java +++ b/src/test/groovy/readme/IncrementalExamples.java @@ -1,10 +1,11 @@ package readme; -import graphql.DeferredExecutionResult; import graphql.Directives; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; +import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.IncrementalExecutionResult; import graphql.schema.GraphQLSchema; import jakarta.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; @@ -13,8 +14,8 @@ import java.util.Map; -@SuppressWarnings({"unused", "ConstantConditions", "UnusedAssignment", "unchecked"}) -public class DeferredExamples { +@SuppressWarnings({"unused", "ConstantConditions"}) +public class IncrementalExamples { GraphQLSchema buildSchemaWithDirective() { @@ -35,18 +36,19 @@ void basicExample(HttpServletResponse httpServletResponse, String deferredQuery) ExecutionResult initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(deferredQuery).build()); // - // then initial results happen first, the deferred ones will begin AFTER these initial + // then initial results happen first, the incremental ones will begin AFTER these initial // results have completed // sendMultipartHttpResult(httpServletResponse, initialResult); Map extensions = initialResult.getExtensions(); - Publisher deferredResults = (Publisher) extensions.get(GraphQL.DEFERRED_RESULTS); + Publisher delayedIncrementalResults = + ((IncrementalExecutionResult) initialResult).getIncrementalItemPublisher(); // - // you subscribe to the deferred results like any other reactive stream + // you subscribe to the incremental results like any other reactive stream // - deferredResults.subscribe(new Subscriber() { + delayedIncrementalResults.subscribe(new Subscriber<>() { Subscription subscription; @@ -59,7 +61,7 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(DeferredExecutionResult executionResult) { + public void onNext(DelayedIncrementalExecutionResult executionResult) { // // as each deferred result arrives, send it to where it needs to go // @@ -85,7 +87,7 @@ private void completeResponse(HttpServletResponse httpServletResponse) { private void handleError(HttpServletResponse httpServletResponse, Throwable t) { } - private void sendMultipartHttpResult(HttpServletResponse httpServletResponse, ExecutionResult initialResult) { + private void sendMultipartHttpResult(HttpServletResponse httpServletResponse, Object result) { } From ca51dcb406f465eff114ec6f3b61c88f6669cfd0 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 23 Jan 2024 18:03:16 +1100 Subject: [PATCH 106/393] Add more tests for DeferContext --- .../execution/defer/DeferContextTest.groovy | 120 +++++++++++++++--- 1 file changed, 99 insertions(+), 21 deletions(-) diff --git a/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy b/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy index 31a0ac3eea..8332525dc2 100644 --- a/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy +++ b/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy @@ -1,6 +1,6 @@ package graphql.execution.defer -import graphql.ExecutionResult + import graphql.ExecutionResultImpl import graphql.execution.ResultPath import graphql.incremental.DelayedIncrementalExecutionResult @@ -12,9 +12,7 @@ import java.util.function.Supplier class DeferContextTest extends Specification { - def "emits N deferred calls - ordering depends on call latency"() { - given: def deferContext = new DeferContext() deferContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last @@ -22,7 +20,7 @@ class DeferContextTest extends Specification { deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = [] + List results = [] def subscriber = new BasicSubscriber() { @Override void onNext(DelayedIncrementalExecutionResult executionResult) { @@ -34,10 +32,10 @@ class DeferContextTest extends Specification { Awaitility.await().untilTrue(subscriber.finished) then: - results.size() == 3 - results[0].incrementalItems[0].data["c"] == "C" - results[1].incrementalItems[0].data["b"] == "B" - results[2].incrementalItems[0].data["a"] == "A" + assertResultsSizeAndHasNextRule(3, results) + results[0].incremental[0].data["c"] == "C" + results[1].incremental[0].data["b"] == "B" + results[2].incremental[0].data["a"] == "A" } def "calls within calls are enqueued correctly"() { @@ -48,7 +46,7 @@ class DeferContextTest extends Specification { deferContext.enqueue(offThreadCallWithinCall(deferContext, "C", "C_Child", 100, "/c")) when: - List results = [] + List results = [] BasicSubscriber subscriber = new BasicSubscriber() { @Override void onNext(DelayedIncrementalExecutionResult executionResult) { @@ -61,13 +59,13 @@ class DeferContextTest extends Specification { Awaitility.await().untilTrue(subscriber.finished) then: - results.size() == 6 - results[0].incrementalItems[0].data["c"] == "C" - results[1].incrementalItems[0].data["c_child"] == "C_Child" - results[2].incrementalItems[0].data["b"] == "B" - results[3].incrementalItems[0].data["a"] == "A" - results[4].incrementalItems[0].data["b_child"] == "B_Child" - results[5].incrementalItems[0].data["a_child"] == "A_Child" + assertResultsSizeAndHasNextRule(6, results) + results[0].incremental[0].data["c"] == "C" + results[1].incremental[0].data["c_child"] == "C_Child" + results[2].incremental[0].data["b"] == "B" + results[3].incremental[0].data["a"] == "A" + results[4].incremental[0].data["b_child"] == "B_Child" + results[5].incremental[0].data["a_child"] == "A_Child" } def "stops at first exception encountered"() { @@ -78,7 +76,7 @@ class DeferContextTest extends Specification { deferContext.enqueue(offThread("C", 10, "/field/path")) when: - List results = [] + List results = [] Throwable thrown = null def subscriber = new BasicSubscriber() { @Override @@ -104,7 +102,7 @@ class DeferContextTest extends Specification { then: thrown.message == "java.lang.RuntimeException: Bang" - results[0].incrementalItems[0].data["c"] == "C" + results[0].incremental[0].data["c"] == "C" } def "you can cancel the subscription"() { @@ -115,7 +113,7 @@ class DeferContextTest extends Specification { deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = [] + List results = [] def subscriber = new BasicSubscriber() { @Override void onNext(DelayedIncrementalExecutionResult executionResult) { @@ -130,7 +128,9 @@ class DeferContextTest extends Specification { then: results.size() == 1 - results[0].incrementalItems[0].data["c"] == "C" + results[0].incremental[0].data["c"] == "C" + // Cancelling the subscription will result in an invalid state. The last result item will have "hasNext=true". + results[0].hasNext } def "you cant subscribe twice"() { @@ -172,11 +172,76 @@ class DeferContextTest extends Specification { } def "multiple fields are part of the same call"() { + given: "a DeferredCall that contains resolution of multiple fields" + def call1 = new Supplier>() { + @Override + CompletableFuture get() { + return CompletableFuture.supplyAsync({ + Thread.sleep(10) + new DeferredCall.FieldWithExecutionResult("call1", new ExecutionResultImpl("Call 1", [])) + }) + } + } + + def call2 = new Supplier>() { + @Override + CompletableFuture get() { + return CompletableFuture.supplyAsync({ + Thread.sleep(100) + new DeferredCall.FieldWithExecutionResult("call2", new ExecutionResultImpl("Call 2", [])) + }) + } + } + + def deferredCall = new DeferredCall(null, ResultPath.parse("/field/path"), [call1, call2], new DeferredErrorSupport()) + + when: + def deferContext = new DeferContext() + deferContext.enqueue(deferredCall) + List results = [] + BasicSubscriber subscriber = new BasicSubscriber() { + @Override + void onNext(DelayedIncrementalExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + } + deferContext.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + then: + assertResultsSizeAndHasNextRule(1, results) + results[0].incremental[0].data["call1"] == "Call 1" + results[0].incremental[0].data["call2"] == "Call 2" } - def "race condition"() { + def "race conditions should not impact the calculation of the hasNext value"() { + given: "calls that have the same sleepTime" + def deferContext = new DeferContext() + deferContext.enqueue(offThread("A", 10, "/field/path")) // <-- will finish last + deferContext.enqueue(offThread("B", 10, "/field/path")) // <-- will finish second + deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + when: + List results = [] + def subscriber = new BasicSubscriber() { + @Override + void onNext(DelayedIncrementalExecutionResult executionResult) { + results.add(executionResult) + subscription.request(1) + } + } + deferContext.startDeferredCalls().subscribe(subscriber) + Awaitility.await().untilTrue(subscriber.finished) + + then: "hasNext placement should be deterministic - only the last event published should have 'hasNext=true'" + assertResultsSizeAndHasNextRule(3, results) + + then: "but the actual order or publish events is non-deterministic - they all have the same latency (sleepTime)." + results.any { it.incremental[0].data["a"] == "A" } + results.any { it.incremental[0].data["b"] == "B" } + results.any { it.incremental[0].data["c"] == "C" } } private static DeferredCall offThread(String data, int sleepTime, String path) { @@ -209,4 +274,17 @@ class DeferContextTest extends Specification { } return new DeferredCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredErrorSupport()) } + + + private static void assertResultsSizeAndHasNextRule(int expectedSize, List results) { + assert results.size() == expectedSize + + for (def i = 0; i < results.size(); i++) { + def isLastResult = i == results.size() - 1 + def hasNext = results[i].hasNext() + + assert (hasNext && !isLastResult) + || (!hasNext && isLastResult) + } + } } From 85ac36317a1c6384d244a00fcdd68f1acae3bc8a Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 24 Jan 2024 10:02:04 +1100 Subject: [PATCH 107/393] Create IncrementalCall interface and add type abstraction --- .../execution/AsyncExecutionStrategy.java | 2 +- .../java/graphql/execution/Execution.java | 4 +- .../graphql/execution/ExecutionContext.java | 8 +- .../graphql/execution/defer/DeferredCall.java | 23 ++++- .../execution/defer/DeferredSelectionSet.java | 22 ----- .../execution/defer/IncrementalCall.java | 14 ++++ ...erContext.java => IncrementalContext.java} | 18 ++-- .../graphql/execution/defer/StreamedCall.java | 19 +++++ ...ovy => IncrementalContextDeferTest.groovy} | 83 +++++++++---------- 9 files changed, 109 insertions(+), 84 deletions(-) delete mode 100644 src/main/java/graphql/execution/defer/DeferredSelectionSet.java create mode 100644 src/main/java/graphql/execution/defer/IncrementalCall.java rename src/main/java/graphql/execution/defer/{DeferContext.java => IncrementalContext.java} (86%) create mode 100644 src/main/java/graphql/execution/defer/StreamedCall.java rename src/test/groovy/graphql/execution/defer/{DeferContextTest.groovy => IncrementalContextDeferTest.groovy} (72%) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index a324b8f08f..122246a9b1 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -69,7 +69,7 @@ public CompletableFuture execute(ExecutionContext executionCont this::resolveFieldWithInfo ) : DeferExecutionSupport.NOOP; - executionContext.getDefferContext().enqueue(deferExecutionSupport.createCalls()); + executionContext.getIncrementalContext().enqueue(deferExecutionSupport.createCalls()); Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size() - deferExecutionSupport.deferredFieldsCount()); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index ec5edb2010..4e3fe78195 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -7,7 +7,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; -import graphql.execution.defer.DeferContext; +import graphql.execution.defer.IncrementalContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -194,7 +194,7 @@ private CompletableFuture executeOperation(ExecutionContext exe */ private CompletableFuture deferSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { - DeferContext deferSupport = executionContext.getDefferContext(); + IncrementalContext deferSupport = executionContext.getIncrementalContext(); if (deferSupport.isDeferDetected()) { // we start the rest of the query now to maximize throughput. We have the initial important results // and now we can start the rest of the calls as early as possible (even before some one subscribes) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index aaa4cfb50a..89f4a4c453 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -8,7 +8,7 @@ import graphql.GraphQLError; import graphql.PublicApi; import graphql.collect.ImmutableKit; -import graphql.execution.defer.DeferContext; +import graphql.execution.defer.IncrementalContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; @@ -54,7 +54,7 @@ public class ExecutionContext { private final Set errorPaths = new HashSet<>(); private final DataLoaderRegistry dataLoaderRegistry; private final Locale locale; - private final DeferContext defferContext = new DeferContext(); + private final IncrementalContext incrementalContext = new IncrementalContext(); private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; @@ -257,8 +257,8 @@ public ExecutionStrategy getSubscriptionStrategy() { return subscriptionStrategy; } - public DeferContext getDefferContext() { - return defferContext; + public IncrementalContext getIncrementalContext() { + return incrementalContext; } public ExecutionStrategy getStrategy(OperationDefinition.Operation operation) { diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index 72a9166071..b114eb65c8 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -14,11 +14,25 @@ import java.util.function.Supplier; /** - * This represents a deferred call (aka @defer) to get an execution result sometime after - * the initial query has returned + * Represents a deferred call (aka @defer) to get an execution result sometime after the initial query has returned. + *

+ * A deferred call can encompass multiple fields. The deferred call will resolve once all sub-fields resolve. + *

+ * For example, this query: + *

+ * {
+ *     post {
+ *         ... @defer(label: "defer-post") {
+ *             text
+ *             summary
+ *         }
+ *     }
+ * }
+ * 
+ * Will result on 1 instance of `DeferredCall`, containing calls for the 2 fields: "text" and "summary". */ @Internal -public class DeferredCall { +public class DeferredCall implements IncrementalCall { private final String label; private final ResultPath path; private final List>> calls; @@ -36,7 +50,8 @@ public DeferredCall( this.errorSupport = deferredErrorSupport; } - CompletableFuture invoke() { + @Override + public CompletableFuture invoke() { Async.CombinedBuilder futures = Async.ofExpectedSize(calls.size()); calls.forEach(call -> futures.add(call.get())); diff --git a/src/main/java/graphql/execution/defer/DeferredSelectionSet.java b/src/main/java/graphql/execution/defer/DeferredSelectionSet.java deleted file mode 100644 index 21a075e2be..0000000000 --- a/src/main/java/graphql/execution/defer/DeferredSelectionSet.java +++ /dev/null @@ -1,22 +0,0 @@ -package graphql.execution.defer; - -import graphql.ExperimentalApi; -import graphql.execution.MergedField; -import graphql.execution.MergedSelectionSet; -import graphql.execution.incremental.DeferExecution; - -import java.util.Map; - -@ExperimentalApi -public class DeferredSelectionSet extends MergedSelectionSet { - private final DeferExecution deferExecution; - - private DeferredSelectionSet(DeferExecution deferExecution, Map subFields) { - super(subFields); - this.deferExecution = deferExecution; - } - - public DeferExecution getDeferExecution() { - return deferExecution; - } -} diff --git a/src/main/java/graphql/execution/defer/IncrementalCall.java b/src/main/java/graphql/execution/defer/IncrementalCall.java new file mode 100644 index 0000000000..c322b0a165 --- /dev/null +++ b/src/main/java/graphql/execution/defer/IncrementalCall.java @@ -0,0 +1,14 @@ +package graphql.execution.defer; + +import graphql.incremental.IncrementalPayload; + +import java.util.concurrent.CompletableFuture; + +/** + * Represents an incremental call (resulted from the usage of @defer or @stream). + * + * @param the type of the payload that this call resolves. + */ +public interface IncrementalCall { + CompletableFuture invoke(); +} diff --git a/src/main/java/graphql/execution/defer/DeferContext.java b/src/main/java/graphql/execution/defer/IncrementalContext.java similarity index 86% rename from src/main/java/graphql/execution/defer/DeferContext.java rename to src/main/java/graphql/execution/defer/IncrementalContext.java index 2ee52f2dd7..02dc84fa80 100644 --- a/src/main/java/graphql/execution/defer/DeferContext.java +++ b/src/main/java/graphql/execution/defer/IncrementalContext.java @@ -3,6 +3,7 @@ import graphql.Internal; import graphql.execution.reactive.SingleSubscriberPublisher; import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.IncrementalPayload; import graphql.util.LockKit; import org.reactivestreams.Publisher; @@ -20,20 +21,19 @@ * the main result is sent via a Publisher stream. */ @Internal -// TODO: This should be called IncrementalSupport and handle both @defer and @stream -public class DeferContext { +public class IncrementalContext { private final AtomicBoolean deferDetected = new AtomicBoolean(false); - private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); + private final Deque> incrementalCalls = new ConcurrentLinkedDeque<>(); private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); private final AtomicInteger pendingCalls = new AtomicInteger(); private final LockKit.ReentrantLock publisherLock = new LockKit.ReentrantLock(); @SuppressWarnings("FutureReturnValueIgnored") private void drainDeferredCalls() { - DeferredCall deferredCall = deferredCalls.poll(); + IncrementalCall incrementalCall = incrementalCalls.poll(); - while (deferredCall != null) { - deferredCall.invoke() + while (incrementalCall != null) { + incrementalCall.invoke() .whenComplete((payload, exception) -> { if (exception != null) { publisher.offerError(exception); @@ -65,20 +65,20 @@ private void drainDeferredCalls() { drainDeferredCalls(); } }); - deferredCall = deferredCalls.poll(); + incrementalCall = incrementalCalls.poll(); } } public void enqueue(DeferredCall deferredCall) { deferDetected.set(true); - deferredCalls.offer(deferredCall); + incrementalCalls.offer(deferredCall); pendingCalls.incrementAndGet(); } public void enqueue(Collection calls) { if (!calls.isEmpty()) { deferDetected.set(true); - deferredCalls.addAll(calls); + incrementalCalls.addAll(calls); pendingCalls.addAndGet(calls.size()); } } diff --git a/src/main/java/graphql/execution/defer/StreamedCall.java b/src/main/java/graphql/execution/defer/StreamedCall.java new file mode 100644 index 0000000000..90badbc9f4 --- /dev/null +++ b/src/main/java/graphql/execution/defer/StreamedCall.java @@ -0,0 +1,19 @@ +package graphql.execution.defer; + +import graphql.Internal; +import graphql.incremental.StreamPayload; + +import java.util.concurrent.CompletableFuture; + +/** + * Represents a call that fetches data that was streamed, via the @stream directive. + *

+ * This is a placeholder class, created to showcase the proposed structure that accommodates both @defer and @stream execution. + */ +@Internal +public class StreamedCall implements IncrementalCall { + @Override + public CompletableFuture invoke() { + throw new UnsupportedOperationException("Not implemented yet."); + } +} diff --git a/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy similarity index 72% rename from src/test/groovy/graphql/execution/defer/DeferContextTest.groovy rename to src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy index 8332525dc2..f4d6ca01f6 100644 --- a/src/test/groovy/graphql/execution/defer/DeferContextTest.groovy +++ b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy @@ -10,14 +10,14 @@ import spock.lang.Specification import java.util.concurrent.CompletableFuture import java.util.function.Supplier -class DeferContextTest extends Specification { +class IncrementalContextDeferTest extends Specification { def "emits N deferred calls - ordering depends on call latency"() { given: - def deferContext = new DeferContext() - deferContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last - deferContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second - deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalContext = new IncrementalContext() + incrementalContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + incrementalContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: List results = [] @@ -28,7 +28,7 @@ class DeferContextTest extends Specification { subscription.request(1) } } - deferContext.startDeferredCalls().subscribe(subscriber) + incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) then: @@ -40,10 +40,10 @@ class DeferContextTest extends Specification { def "calls within calls are enqueued correctly"() { given: - def deferContext = new DeferContext() - deferContext.enqueue(offThreadCallWithinCall(deferContext, "A", "A_Child", 500, "/a")) - deferContext.enqueue(offThreadCallWithinCall(deferContext, "B", "B_Child", 300, "/b")) - deferContext.enqueue(offThreadCallWithinCall(deferContext, "C", "C_Child", 100, "/c")) + def incrementalContext = new IncrementalContext() + incrementalContext.enqueue(offThreadCallWithinCall(incrementalContext, "A", "A_Child", 500, "/a")) + incrementalContext.enqueue(offThreadCallWithinCall(incrementalContext, "B", "B_Child", 300, "/b")) + incrementalContext.enqueue(offThreadCallWithinCall(incrementalContext, "C", "C_Child", 100, "/c")) when: List results = [] @@ -54,7 +54,7 @@ class DeferContextTest extends Specification { subscription.request(1) } } - deferContext.startDeferredCalls().subscribe(subscriber) + incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) then: @@ -70,10 +70,10 @@ class DeferContextTest extends Specification { def "stops at first exception encountered"() { given: - def deferContext = new DeferContext() - deferContext.enqueue(offThread("A", 100, "/field/path")) - deferContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception - deferContext.enqueue(offThread("C", 10, "/field/path")) + def incrementalContext = new IncrementalContext() + incrementalContext.enqueue(offThread("A", 100, "/field/path")) + incrementalContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception + incrementalContext.enqueue(offThread("C", 10, "/field/path")) when: List results = [] @@ -96,7 +96,7 @@ class DeferContextTest extends Specification { assert false, "This should not be called!" } } - deferContext.startDeferredCalls().subscribe(subscriber) + incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) then: @@ -107,10 +107,10 @@ class DeferContextTest extends Specification { def "you can cancel the subscription"() { given: - def deferContext = new DeferContext() - deferContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last - deferContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second - deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalContext = new IncrementalContext() + incrementalContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + incrementalContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: List results = [] @@ -122,7 +122,7 @@ class DeferContextTest extends Specification { finished.set(true) } } - deferContext.startDeferredCalls().subscribe(subscriber) + incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) then: @@ -135,15 +135,15 @@ class DeferContextTest extends Specification { def "you cant subscribe twice"() { given: - def deferContext = new DeferContext() - deferContext.enqueue(offThread("A", 100, "/field/path")) - deferContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second - deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalContext = new IncrementalContext() + incrementalContext.enqueue(offThread("A", 100, "/field/path")) + incrementalContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second + incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: Throwable expectedThrowable - deferContext.startDeferredCalls().subscribe(new BasicSubscriber()) - deferContext.startDeferredCalls().subscribe(new BasicSubscriber() { + incrementalContext.startDeferredCalls().subscribe(new BasicSubscriber()) + incrementalContext.startDeferredCalls().subscribe(new BasicSubscriber() { @Override void onError(Throwable t) { expectedThrowable = t @@ -155,17 +155,17 @@ class DeferContextTest extends Specification { def "indicates if there are any defers present"() { given: - def deferContext = new DeferContext() + def incrementalContext = new IncrementalContext() when: - def deferPresent1 = deferContext.isDeferDetected() + def deferPresent1 = incrementalContext.isDeferDetected() then: !deferPresent1 when: - deferContext.enqueue(offThread("A", 100, "/field/path")) - def deferPresent2 = deferContext.isDeferDetected() + incrementalContext.enqueue(offThread("A", 100, "/field/path")) + def deferPresent2 = incrementalContext.isDeferDetected() then: deferPresent2 @@ -196,8 +196,8 @@ class DeferContextTest extends Specification { def deferredCall = new DeferredCall(null, ResultPath.parse("/field/path"), [call1, call2], new DeferredErrorSupport()) when: - def deferContext = new DeferContext() - deferContext.enqueue(deferredCall) + def incrementalContext = new IncrementalContext() + incrementalContext.enqueue(deferredCall) List results = [] BasicSubscriber subscriber = new BasicSubscriber() { @@ -207,7 +207,7 @@ class DeferContextTest extends Specification { subscription.request(1) } } - deferContext.startDeferredCalls().subscribe(subscriber) + incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) then: @@ -218,10 +218,10 @@ class DeferContextTest extends Specification { def "race conditions should not impact the calculation of the hasNext value"() { given: "calls that have the same sleepTime" - def deferContext = new DeferContext() - deferContext.enqueue(offThread("A", 10, "/field/path")) // <-- will finish last - deferContext.enqueue(offThread("B", 10, "/field/path")) // <-- will finish second - deferContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalContext = new IncrementalContext() + incrementalContext.enqueue(offThread("A", 10, "/field/path")) // <-- will finish last + incrementalContext.enqueue(offThread("B", 10, "/field/path")) // <-- will finish second + incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: List results = [] @@ -232,7 +232,7 @@ class DeferContextTest extends Specification { subscription.request(1) } } - deferContext.startDeferredCalls().subscribe(subscriber) + incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) then: "hasNext placement should be deterministic - only the last event published should have 'hasNext=true'" @@ -261,13 +261,13 @@ class DeferContextTest extends Specification { return new DeferredCall(null, ResultPath.parse(path), [callSupplier], new DeferredErrorSupport()) } - private static DeferredCall offThreadCallWithinCall(DeferContext deferContext, String dataParent, String dataChild, int sleepTime, String path) { + private static DeferredCall offThreadCallWithinCall(IncrementalContext incrementalContext, String dataParent, String dataChild, int sleepTime, String path) { def callSupplier = new Supplier>() { @Override CompletableFuture get() { CompletableFuture.supplyAsync({ Thread.sleep(sleepTime) - deferContext.enqueue(offThread(dataChild, sleepTime, path)) + incrementalContext.enqueue(offThread(dataChild, sleepTime, path)) new DeferredCall.FieldWithExecutionResult(dataParent.toLowerCase(), new ExecutionResultImpl(dataParent, [])) }) } @@ -275,7 +275,6 @@ class DeferContextTest extends Specification { return new DeferredCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredErrorSupport()) } - private static void assertResultsSizeAndHasNextRule(int expectedSize, List results) { assert results.size() == expectedSize From 1c60c37f83f9a1765a15ad1f4e20f3a117982839 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 25 Jan 2024 16:18:47 +1100 Subject: [PATCH 108/393] Implement error handling in the defer execution code --- .../execution/AsyncExecutionStrategy.java | 7 +- .../java/graphql/execution/Execution.java | 2 +- .../graphql/execution/ExecutionContext.java | 4 + .../graphql/execution/ExecutionStrategy.java | 3 +- .../ExecutionStrategyParameters.java | 12 +- .../graphql/execution/defer/DeferredCall.java | 41 ++- ...rSupport.java => DeferredCallContext.java} | 17 +- .../AsyncExecutionStrategyTest.groovy | 27 +- .../defer/CapturingSubscriber.groovy | 2 + .../execution/defer/DeferredCallTest.groovy | 54 ++- .../defer/IncrementalContextDeferTest.groovy | 6 +- ...eferExecutionSupportIntegrationTest.groovy | 320 +++++++++++++++++- .../DeferredMustBeOnAllFieldsTest.groovy | 193 ----------- 13 files changed, 459 insertions(+), 229 deletions(-) rename src/main/java/graphql/execution/defer/{DeferredErrorSupport.java => DeferredCallContext.java} (50%) delete mode 100644 src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 122246a9b1..850a799361 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -6,7 +6,7 @@ import graphql.ExecutionResult; import graphql.PublicApi; import graphql.execution.defer.DeferredCall; -import graphql.execution.defer.DeferredErrorSupport; +import graphql.execution.defer.DeferredCallContext; import graphql.execution.incremental.DeferExecution; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; @@ -52,7 +52,6 @@ public AsyncExecutionStrategy(DataFetcherExceptionHandler exceptionHandler) { @Override @SuppressWarnings("FutureReturnValueIgnored") public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { - Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); @@ -61,7 +60,7 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); - DeferExecutionSupport deferExecutionSupport = executionContext.getExecutionInput().isIncrementalSupport() ? + DeferExecutionSupport deferExecutionSupport = executionContext.isIncrementalSupport() ? new DeferExecutionSupport.DeferExecutionSupportImpl( fields, parameters, @@ -189,7 +188,7 @@ public List getNonDeferredFieldNames(List allFieldNames) { public Set createCalls() { return deferExecutionToFields.keySet().stream() .map(deferExecution -> { - DeferredErrorSupport errorSupport = new DeferredErrorSupport(); + DeferredCallContext errorSupport = new DeferredCallContext(); List mergedFields = deferExecutionToFields.get(deferExecution); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 4e3fe78195..161babf86a 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -144,7 +144,7 @@ private CompletableFuture executeOperation(ExecutionContext exe MergedSelectionSet fields = fieldCollector.collectFields( collectorParameters, operationDefinition.getSelectionSet(), - executionContext.getExecutionInput().isIncrementalSupport() + executionContext.isIncrementalSupport() ); ResultPath path = ResultPath.rootPath(); diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 89f4a4c453..1730aa9be0 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -261,6 +261,10 @@ public IncrementalContext getIncrementalContext() { return incrementalContext; } + public boolean isIncrementalSupport() { + return executionInput != null && executionInput.isIncrementalSupport(); + } + public ExecutionStrategy getStrategy(OperationDefinition.Operation operation) { if (operation == OperationDefinition.Operation.MUTATION) { return getMutationStrategy(); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 96f5d76a94..5ad5bb8729 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -372,7 +372,6 @@ protected CompletableFuture handleFetchingException( .exception(e) .build(); - // TODO: not sure if this method call goes here, inside the try block below, or in the async method parameters.deferredErrorSupport().onFetchingException(parameters, e); try { @@ -692,7 +691,7 @@ protected CompletableFuture completeValueForObject(ExecutionCon MergedSelectionSet subFields = fieldCollector.collectFields( collectorParameters, parameters.getField(), - executionContext.getExecutionInput().isIncrementalSupport() + executionContext.isIncrementalSupport() ); ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull(resolvedObjectType); diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index 42b6db6572..cfa76271a4 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -2,7 +2,7 @@ import graphql.Assert; import graphql.PublicApi; -import graphql.execution.defer.DeferredErrorSupport; +import graphql.execution.defer.DeferredCallContext; import java.util.function.Consumer; @@ -21,7 +21,7 @@ public class ExecutionStrategyParameters { private final ResultPath path; private final MergedField currentField; private final ExecutionStrategyParameters parent; - private final DeferredErrorSupport deferredErrorSupport; + private final DeferredCallContext deferredErrorSupport; private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, Object source, @@ -31,7 +31,7 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, ResultPath path, MergedField currentField, ExecutionStrategyParameters parent, - DeferredErrorSupport deferredErrorSupport) { + DeferredCallContext deferredErrorSupport) { this.executionStepInfo = assertNotNull(executionStepInfo, () -> "executionStepInfo is null"); this.localContext = localContext; @@ -72,7 +72,7 @@ public ExecutionStrategyParameters getParent() { return parent; } - public DeferredErrorSupport deferredErrorSupport() { + public DeferredCallContext deferredErrorSupport() { return deferredErrorSupport; } @@ -114,7 +114,7 @@ public static class Builder { ResultPath path = ResultPath.rootPath(); MergedField currentField; ExecutionStrategyParameters parent; - DeferredErrorSupport deferredErrorSupport = new DeferredErrorSupport(); + DeferredCallContext deferredErrorSupport = new DeferredCallContext(); /** * @see ExecutionStrategyParameters#newParameters() @@ -182,7 +182,7 @@ public Builder parent(ExecutionStrategyParameters parent) { return this; } - public Builder deferredErrorSupport(DeferredErrorSupport deferredErrorSupport) { + public Builder deferredErrorSupport(DeferredCallContext deferredErrorSupport) { this.deferredErrorSupport = deferredErrorSupport; return this; } diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index b114eb65c8..2f2a6ecfd3 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -1,16 +1,21 @@ package graphql.execution.defer; +import com.google.common.collect.ImmutableList; import graphql.ExecutionResult; import graphql.GraphQLError; import graphql.Internal; import graphql.execution.Async; +import graphql.execution.NonNullableFieldWasNullError; +import graphql.execution.NonNullableFieldWasNullException; import graphql.execution.ResultPath; import graphql.incremental.DeferPayload; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.function.Supplier; /** @@ -36,13 +41,13 @@ public class DeferredCall implements IncrementalCall { private final String label; private final ResultPath path; private final List>> calls; - private final DeferredErrorSupport errorSupport; + private final DeferredCallContext errorSupport; public DeferredCall( String label, ResultPath path, List>> calls, - DeferredErrorSupport deferredErrorSupport + DeferredCallContext deferredErrorSupport ) { this.label = label; this.path = path; @@ -57,17 +62,45 @@ public CompletableFuture invoke() { calls.forEach(call -> futures.add(call.get())); return futures.await() - .thenApply(this::transformToDeferredPayload); + .thenApply(this::transformToDeferredPayload) + .handle(this::handleNonNullableFieldError); + } + + /** + * Non-nullable errors need special treatment. + * When they happen, all the sibling fields will be ignored in the result. So as soon as one of the field calls + * throw this error, we can ignore the {@link ExecutionResult} from all the fields associated with this {@link DeferredCall} + * and build a special {@link DeferPayload} that captures the details of the error. + */ + private DeferPayload handleNonNullableFieldError(DeferPayload result, Throwable throwable) { + if (throwable != null) { + Throwable cause = throwable.getCause(); + if (cause instanceof NonNullableFieldWasNullException) { + GraphQLError error = new NonNullableFieldWasNullError((NonNullableFieldWasNullException) cause); + return DeferPayload.newDeferredItem() + .errors(Collections.singletonList(error)) + .label(label) + .path(path) + .build(); + } + if (cause instanceof CompletionException) { + throw (CompletionException) cause; + } + throw new CompletionException(cause); + } + return result; } private DeferPayload transformToDeferredPayload(List fieldWithExecutionResults) { - // TODO: Not sure how/if this errorSupport works List errorsEncountered = errorSupport.getErrors(); Map dataMap = new HashMap<>(); + ImmutableList.Builder errorsBuilder = ImmutableList.builder(); + fieldWithExecutionResults.forEach(entry -> { dataMap.put(entry.fieldName, entry.executionResult.getData()); + errorsBuilder.addAll(entry.executionResult.getErrors()); }); return DeferPayload.newDeferredItem() diff --git a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java b/src/main/java/graphql/execution/defer/DeferredCallContext.java similarity index 50% rename from src/main/java/graphql/execution/defer/DeferredErrorSupport.java rename to src/main/java/graphql/execution/defer/DeferredCallContext.java index 3ef4f34c5d..c9ea3dde32 100644 --- a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java +++ b/src/main/java/graphql/execution/defer/DeferredCallContext.java @@ -9,10 +9,16 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * This captures errors that occur while a deferred call is being made + * Contains data relevant to the execution of a {@link DeferredCall}. + *

+ * The responsibilities of this class are similar to {@link graphql.execution.ExecutionContext}, but restricted to the + * execution of a deferred call (instead of the whole GraphQL execution like {@link graphql.execution.ExecutionContext}). + *

+ * Some behaviours, like error capturing, need to be scoped to a single {@link DeferredCall}, because each defer payload + * contains its own distinct list of errors. */ @Internal -public class DeferredErrorSupport { +public class DeferredCallContext { private final List errors = new CopyOnWriteArrayList<>(); @@ -21,10 +27,13 @@ public void onFetchingException(ExecutionStrategyParameters parameters, Throwabl onError(error); } - public void onError(GraphQLError gError) { - errors.add(gError); + public void onError(GraphQLError graphqlError) { + errors.add(graphqlError); } + /** + * @return a list of errors that were encountered while executing this deferred call + */ public List getErrors() { return errors; } diff --git a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy index 951246df0c..722b674f1e 100644 --- a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy @@ -1,6 +1,7 @@ package graphql.execution import graphql.ErrorType +import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQLContext import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext @@ -30,7 +31,10 @@ import static graphql.schema.GraphQLObjectType.newObject import static graphql.schema.GraphQLSchema.newSchema import static org.awaitility.Awaitility.await -class AsyncExecutionStrategyTest extends Specification { +abstract class AsyncExecutionStrategyTest extends Specification { + static boolean incrementalSupport + + def executionInputMock = Mock(ExecutionInput) GraphQLSchema schema(DataFetcher dataFetcher1, DataFetcher dataFetcher2) { def queryName = "RootQueryType" @@ -65,6 +69,10 @@ class AsyncExecutionStrategyTest extends Specification { schema } + def setup() { + executionInputMock.isIncrementalSupport() >> incrementalSupport + } + def "execution is serial if the dataFetchers are blocking"() { given: def lock = new ReentrantLock() @@ -100,6 +108,7 @@ class AsyncExecutionStrategyTest extends Specification { .valueUnboxer(ValueUnboxer.DEFAULT) .graphQLContext(GraphQLContext.getDefault()) .locale(Locale.getDefault()) + .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -141,6 +150,7 @@ class AsyncExecutionStrategyTest extends Specification { .instrumentation(SimplePerformantInstrumentation.INSTANCE) .locale(Locale.getDefault()) .graphQLContext(GraphQLContext.getDefault()) + .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -184,6 +194,7 @@ class AsyncExecutionStrategyTest extends Specification { .instrumentation(SimplePerformantInstrumentation.INSTANCE) .graphQLContext(GraphQLContext.getDefault()) .locale(Locale.getDefault()) + .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -226,6 +237,7 @@ class AsyncExecutionStrategyTest extends Specification { .valueUnboxer(ValueUnboxer.DEFAULT) .locale(Locale.getDefault()) .graphQLContext(GraphQLContext.getDefault()) + .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -287,6 +299,7 @@ class AsyncExecutionStrategyTest extends Specification { } } }) + .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -311,3 +324,15 @@ class AsyncExecutionStrategyTest extends Specification { } + +class AsyncExecutionStrategyTestWithIncrementalSupport extends AsyncExecutionStrategyTest { + static { + incrementalSupport = true + } +} + +class AsyncExecutionStrategyTestNoIncrementalSupport extends AsyncExecutionStrategyTest { + static { + incrementalSupport = false + } +} diff --git a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy index 46d0b63046..0334dc3d24 100644 --- a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy +++ b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy @@ -40,6 +40,8 @@ class CapturingSubscriber implements Subscriber> createResolvedFieldCall( + String fieldName, + Object data + ) { + return new Supplier>() { + @Override + CompletableFuture get() { + return completedFuture( + new DeferredCall.FieldWithExecutionResult(fieldName, + new ExecutionResultImpl(data, Collections.emptyList()) + ) + ) + } + } + } } diff --git a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy index f4d6ca01f6..854382119c 100644 --- a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy +++ b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy @@ -193,7 +193,7 @@ class IncrementalContextDeferTest extends Specification { } } - def deferredCall = new DeferredCall(null, ResultPath.parse("/field/path"), [call1, call2], new DeferredErrorSupport()) + def deferredCall = new DeferredCall(null, ResultPath.parse("/field/path"), [call1, call2], new DeferredCallContext()) when: def incrementalContext = new IncrementalContext() @@ -258,7 +258,7 @@ class IncrementalContextDeferTest extends Specification { } } - return new DeferredCall(null, ResultPath.parse(path), [callSupplier], new DeferredErrorSupport()) + return new DeferredCall(null, ResultPath.parse(path), [callSupplier], new DeferredCallContext()) } private static DeferredCall offThreadCallWithinCall(IncrementalContext incrementalContext, String dataParent, String dataChild, int sleepTime, String path) { @@ -272,7 +272,7 @@ class IncrementalContextDeferTest extends Specification { }) } } - return new DeferredCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredErrorSupport()) + return new DeferredCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredCallContext()) } private static void assertResultsSizeAndHasNextRule(int expectedSize, List results) { diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 405df81c1e..93c8123db2 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -50,6 +50,10 @@ class DeferExecutionSupportIntegrationTest extends Specification { latestComment: Comment comments: [Comment] resolvesToNull: String + dataFetcherError: String + coercionError: Int + typeMismatchError: [String] + nonNullableError: String! } type Comment { @@ -93,11 +97,9 @@ class DeferExecutionSupportIntegrationTest extends Specification { private static DataFetcher resolveItem() { return (env) -> { - def data = env.getArgument("type") == "Post" - ? [__typename: "Post", id: "1001"] - : [__typename: "Page", id: "1002"] + def type = env.getArgument("type") - return CompletableFuture.supplyAsync { data } + return CompletableFuture.supplyAsync { [__typename: type, id: "1001"] } } } @@ -107,6 +109,15 @@ class DeferExecutionSupportIntegrationTest extends Specification { } } + private static DataFetcher resolveWithException() { + return new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + throw new RuntimeException("Bang!!!") + } + } + } + void setup() { def runtimeWiring = RuntimeWiring.newRuntimeWiring() .type(newTypeWiring("Query") @@ -117,6 +128,10 @@ class DeferExecutionSupportIntegrationTest extends Specification { .type(newTypeWiring("Post").dataFetcher("summary", resolve("A summary", 10))) .type(newTypeWiring("Post").dataFetcher("text", resolve("The full text", 100))) .type(newTypeWiring("Post").dataFetcher("latestComment", resolve([title: "Comment title"], 10))) + .type(newTypeWiring("Post").dataFetcher("dataFetcherError", resolveWithException())) + .type(newTypeWiring("Post").dataFetcher("coercionError", resolve("Not a number", 10))) + .type(newTypeWiring("Post").dataFetcher("typeMismatchError", resolve([a: "A Map instead of a List"], 10))) + .type(newTypeWiring("Post").dataFetcher("nonNullableError", resolve(null))) .type(newTypeWiring("Page").dataFetcher("summary", resolve("A page summary", 10))) .type(newTypeWiring("Page").dataFetcher("text", resolve("The page full text", 100))) .type(newTypeWiring("Comment").dataFetcher("content", resolve("Full content", 100))) @@ -204,7 +219,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } else { assert initialResult.toSpecification() == [ - data : [item: [__typename: "Page", id: "1002"]], + data : [item: [__typename: "Page", id: "1001"]], hasNext: true ] } @@ -927,6 +942,301 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "can handle error raised by data fetcher"() { + def query = ''' + query { + post { + id + ... @defer { + dataFetcherError + } + ... @defer { + text + } + } + } + ''' + + when: + def initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path : ["post"], + data : [dataFetcherError: null], + errors: [[ + message : "Exception while fetching data (/post/dataFetcherError) : Bang!!!", + locations : [[line: 6, column: 25]], + path : ["post", "dataFetcherError"], + extensions: [classification: "DataFetchingException"] + ]], + ], + ] + ], + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [text: "The full text"], + ] + ] + ] + ] + } + + def "can handle UnresolvedTypeException"() { + def query = """ + query { + post { + id + ... @defer { + text + } + } + ... @defer { + item(type: "NonExistingType") { + id + summary + } + } + } + """ + + when: + def initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path : [], + data : [item: null], + errors: [ + [ + message : "Can't resolve '/item'. Abstract type 'Item' must resolve to an Object type at runtime for field 'Query.item'. Could not determine the exact type of 'Item'", + path : ["item"], + extensions: [ + classification: "DataFetchingException" + ] + ] + ], + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + data : [text: "The full text"], + ] + ] + ] + ] + } + + def "can handle coercion problem"() { + def query = """ + query { + post { + id + ... @defer { + text + } + ... @defer { + coercionError + } + } + } + """ + + when: + def initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path : ["post"], + data : [coercionError: null], + errors: [ + [ + message : "Can't serialize value (/post/coercionError) : Expected a value that can be converted to type 'Int' but it was a 'String'", + path : ["post", "coercionError"], + extensions: [ + classification: "DataFetchingException" + ] + ] + ], + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + data : [text: "The full text"], + ] + ] + ] + ] + } + + def "can handle type mismatch problem"() { + def query = """ + query { + post { + id + ... @defer { + text + } + ... @defer { + typeMismatchError + } + } + } + """ + + when: + def initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path : ["post"], + data : [typeMismatchError: null], + errors: [ + [ + message : "Can't resolve value (/post/typeMismatchError) : type mismatch error, expected type LIST", + path : ["post", "typeMismatchError"], + extensions: [ + classification: "DataFetchingException" + ] + ] + ], + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + data : [text: "The full text"], + ] + ] + ] + ] + } + + def "can handle non nullable error"() { + def query = """ + query { + post { + id + ... @defer { + text + } + ... @defer { + summary + nonNullableError + } + } + } + """ + + when: + def initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path : ["post"], + errors: [ + [ + message : "The field at path '/post/nonNullableError' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'String' within parent type 'Post'", + path : ["post", "nonNullableError"], + extensions: [ + classification: "NullValueInNonNullableField" + ] + ] + ], + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + data : [text: "The full text"], + ] + ] + ] + ] + } + private ExecutionResult executeQuery(String query) { return this.executeQuery(query, true, [:]) } diff --git a/src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy b/src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy deleted file mode 100644 index 9a7c3dcfe5..0000000000 --- a/src/test/groovy/graphql/validation/rules/DeferredMustBeOnAllFieldsTest.groovy +++ /dev/null @@ -1,193 +0,0 @@ -package graphql.validation.rules - -import graphql.Directives -import graphql.GraphQL -import graphql.TestUtil -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorType -import spock.lang.Specification - -class DeferredMustBeOnAllFieldsTest extends Specification { - - def schema = TestUtil.schema(''' - type Query { - newsFeed : NewsFeed - } - - type NewsFeed { - stories : [Story] - } - - type Story { - id : ID - text : String - } - - ''').transform({ it.additionalDirective(Directives.DeferDirective) }) - - - def "all fields MUST contain @defer on all declarations"() { - - def graphQL = GraphQL.newGraphQL(schema).build() - - def query = """ - fragment StoryDetail on Story { - id - text @defer - } - query { - newsFeed { - stories { - text @defer - - # fragment - ...StoryDetail - - ## inline fragment - ... on Story { - id - text # @defer is missing - } - } - } - } - """ - - - when: - def er = graphQL.execute(query) - then: - er.errors.size() == 1 - (er.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferMustBeOnAllFields - (er.errors[0] as ValidationError).queryPath == ["newsFeed", "stories"] - - when: - query = """ - fragment StoryDetail on Story { - id - text # @defer is missing - } - query { - newsFeed { - stories { - text @defer - - # fragment - ...StoryDetail - - ## inline fragment - ... on Story { - id - text @defer - } - } - } - } - """ - - er = graphQL.execute(query) - - then: - er.errors.size() == 1 - (er.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferMustBeOnAllFields - (er.errors[0] as ValidationError).queryPath == ["newsFeed", "stories"] - - when: - query = """ - fragment StoryDetail on Story { - id - text @defer - } - query { - newsFeed { - stories { - text # @defer is missing - - # fragment - ...StoryDetail - - ## inline fragment - ... on Story { - id - text @defer - } - } - } - } - """ - - er = graphQL.execute(query) - - then: - er.errors.size() == 1 - (er.errors[0] as ValidationError).validationErrorType == ValidationErrorType.DeferMustBeOnAllFields - (er.errors[0] as ValidationError).queryPath == ["newsFeed", "stories"] - - } - - def "if all fields contain @defer then its ok"() { - - def graphQL = GraphQL.newGraphQL(schema).build() - - def query = """ - fragment StoryDetail on Story { - id - text @defer - } - query { - newsFeed { - stories { - text @defer - - # fragment - ...StoryDetail - - ## inline fragment - ... on Story { - id - text @defer - } - } - } - } - """ - - - when: - def er = graphQL.execute(query) - then: - er.errors.size() == 0 - } - - def "if only one field contain @defer then its ok"() { - - def graphQL = GraphQL.newGraphQL(schema).build() - - def query = """ - fragment StoryDetail on Story { - id - text @defer - } - query { - newsFeed { - stories { - id - - # fragment - ...StoryDetail - - ## inline fragment - ... on Story { - id - } - } - } - } - """ - - when: - def er = graphQL.execute(query) - then: - er.errors.size() == 0 - } -} From 276c8723dcd24b8c2290d3ad4716bad760cb753d Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 25 Jan 2024 16:23:36 +1100 Subject: [PATCH 109/393] Add one more test case --- ...eferExecutionSupportIntegrationTest.groovy | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 93c8123db2..e7e1bfd2b8 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -1178,7 +1178,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } - def "can handle non nullable error"() { + def "can handle non nullable error in one of the defer calls"() { def query = """ query { post { @@ -1237,6 +1237,51 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "can handle non nullable error in the initial result"() { + def query = """ + query { + post { + id + nonNullableError + ... @defer { + summary + } + } + } + """ + + when: + def initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: null], + errors : [ + [message : "The field at path '/post/nonNullableError' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'String' within parent type 'Post'", + path : ["post", "nonNullableError"], + extensions: [classification: "NullValueInNonNullableField"] + ] + ], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["post"], + data: [summary: "A summary"], + ] + ] + ] + ] + } + private ExecutionResult executeQuery(String query) { return this.executeQuery(query, true, [:]) } From 780f930bf6638987a73c5c8d9aeec3ba6b6ed11a Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sat, 27 Jan 2024 12:45:37 +1100 Subject: [PATCH 110/393] Check in Gradle files for Gradle 8.5 --- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 1 + gradlew | 29 +++++++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! literal 61608 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfjMp+gu>DraHZJRrdO53(= z+o-f{+qNog+qSLB%KY;5>Av6X(>-qYk3IIEwZ5~6a+P9lMpC^ z8CJ0q>rEpjlsxCvJm=kms@tlN4+sv}He`xkr`S}bGih4t`+#VEIt{1veE z{ZLtb_pSbcfcYPf4=T1+|BtR!x5|X#x2TZEEkUB6kslKAE;x)*0x~ES0kl4Dex4e- zT2P~|lT^vUnMp{7e4OExfxak0EE$Hcw;D$ehTV4a6hqxru0$|Mo``>*a5=1Ym0u>BDJKO|=TEWJ5jZu!W}t$Kv{1!q`4Sn7 zrxRQOt>^6}Iz@%gA3&=5r;Lp=N@WKW;>O!eGIj#J;&>+3va^~GXRHCY2}*g#9ULab zitCJt-OV0*D_Q3Q`p1_+GbPxRtV_T`jyATjax<;zZ?;S+VD}a(aN7j?4<~>BkHK7bO8_Vqfdq1#W&p~2H z&w-gJB4?;Q&pG9%8P(oOGZ#`!m>qAeE)SeL*t8KL|1oe;#+uOK6w&PqSDhw^9-&Fa zuEzbi!!7|YhlWhqmiUm!muO(F8-F7|r#5lU8d0+=;<`{$mS=AnAo4Zb^{%p}*gZL! zeE!#-zg0FWsSnablw!9$<&K(#z!XOW z;*BVx2_+H#`1b@>RtY@=KqD)63brP+`Cm$L1@ArAddNS1oP8UE$p05R=bvZoYz+^6 z<)!v7pRvi!u_-V?!d}XWQR1~0q(H3{d^4JGa=W#^Z<@TvI6J*lk!A zZ*UIKj*hyO#5akL*Bx6iPKvR3_2-^2mw|Rh-3O_SGN3V9GRo52Q;JnW{iTGqb9W99 z7_+F(Op6>~3P-?Q8LTZ-lwB}xh*@J2Ni5HhUI3`ct|*W#pqb>8i*TXOLn~GlYECIj zhLaa_rBH|1jgi(S%~31Xm{NB!30*mcsF_wgOY2N0XjG_`kFB+uQuJbBm3bIM$qhUyE&$_u$gb zpK_r{99svp3N3p4yHHS=#csK@j9ql*>j0X=+cD2dj<^Wiu@i>c_v zK|ovi7}@4sVB#bzq$n3`EgI?~xDmkCW=2&^tD5RuaSNHf@Y!5C(Is$hd6cuyoK|;d zO}w2AqJPS`Zq+(mc*^%6qe>1d&(n&~()6-ZATASNPsJ|XnxelLkz8r1x@c2XS)R*H(_B=IN>JeQUR;T=i3<^~;$<+8W*eRKWGt7c#>N`@;#!`kZ!P!&{9J1>_g8Zj zXEXxmA=^{8A|3=Au+LfxIWra)4p<}1LYd_$1KI0r3o~s1N(x#QYgvL4#2{z8`=mXy zQD#iJ0itk1d@Iy*DtXw)Wz!H@G2St?QZFz zVPkM%H8Cd2EZS?teQN*Ecnu|PrC!a7F_XX}AzfZl3fXfhBtc2-)zaC2eKx*{XdM~QUo4IwcGgVdW69 z1UrSAqqMALf^2|(I}hgo38l|Ur=-SC*^Bo5ej`hb;C$@3%NFxx5{cxXUMnTyaX{>~ zjL~xm;*`d08bG_K3-E+TI>#oqIN2=An(C6aJ*MrKlxj?-;G zICL$hi>`F%{xd%V{$NhisHSL~R>f!F7AWR&7b~TgLu6!3s#~8|VKIX)KtqTH5aZ8j zY?wY)XH~1_a3&>#j7N}0az+HZ;is;Zw(Am{MX}YhDTe(t{ZZ;TG}2qWYO+hdX}vp9 z@uIRR8g#y~-^E`Qyem(31{H0&V?GLdq9LEOb2(ea#e-$_`5Q{T%E?W(6 z(XbX*Ck%TQM;9V2LL}*Tf`yzai{0@pYMwBu%(I@wTY!;kMrzcfq0w?X`+y@0ah510 zQX5SU(I!*Fag4U6a7Lw%LL;L*PQ}2v2WwYF(lHx_Uz2ceI$mnZ7*eZ?RFO8UvKI0H z9Pq-mB`mEqn6n_W9(s~Jt_D~j!Ln9HA)P;owD-l~9FYszs)oEKShF9Zzcmnb8kZ7% zQ`>}ki1kwUO3j~ zEmh140sOkA9v>j@#56ymn_RnSF`p@9cO1XkQy6_Kog?0ivZDb`QWOX@tjMd@^Qr(p z!sFN=A)QZm!sTh(#q%O{Ovl{IxkF!&+A)w2@50=?a-+VuZt6On1;d4YtUDW{YNDN_ zG@_jZi1IlW8cck{uHg^g=H58lPQ^HwnybWy@@8iw%G! zwB9qVGt_?~M*nFAKd|{cGg+8`+w{j_^;nD>IrPf-S%YjBslSEDxgKH{5p)3LNr!lD z4ii)^%d&cCXIU7UK?^ZQwmD(RCd=?OxmY(Ko#+#CsTLT;p#A%{;t5YpHFWgl+@)N1 zZ5VDyB;+TN+g@u~{UrWrv)&#u~k$S&GeW)G{M#&Di)LdYk?{($Cq zZGMKeYW)aMtjmKgvF0Tg>Mmkf9IB#2tYmH-s%D_9y3{tfFmX1BSMtbe<(yqAyWX60 zzkgSgKb3c{QPG2MalYp`7mIrYg|Y<4Jk?XvJK)?|Ecr+)oNf}XLPuTZK%W>;<|r+% zTNViRI|{sf1v7CsWHvFrkQ$F7+FbqPQ#Bj7XX=#M(a~9^80}~l-DueX#;b}Ajn3VE z{BWI}$q{XcQ3g{(p>IOzFcAMDG0xL)H%wA)<(gl3I-oVhK~u_m=hAr&oeo|4lZbf} z+pe)c34Am<=z@5!2;_lwya;l?xV5&kWe}*5uBvckm(d|7R>&(iJNa6Y05SvlZcWBlE{{%2- z`86)Y5?H!**?{QbzGG~|k2O%eA8q=gxx-3}&Csf6<9BsiXC)T;x4YmbBIkNf;0Nd5 z%whM^!K+9zH>on_<&>Ws?^v-EyNE)}4g$Fk?Z#748e+GFp)QrQQETx@u6(1fk2!(W zWiCF~MomG*y4@Zk;h#2H8S@&@xwBIs|82R*^K(i*0MTE%Rz4rgO&$R zo9Neb;}_ulaCcdn3i17MO3NxzyJ=l;LU*N9ztBJ30j=+?6>N4{9YXg$m=^9@Cl9VY zbo^{yS@gU=)EpQ#;UIQBpf&zfCA;00H-ee=1+TRw@(h%W=)7WYSb5a%$UqNS@oI@= zDrq|+Y9e&SmZrH^iA>Of8(9~Cf-G(P^5Xb%dDgMMIl8gk6zdyh`D3OGNVV4P9X|EvIhplXDld8d z^YWtYUz@tpg*38Xys2?zj$F8%ivA47cGSl;hjD23#*62w3+fwxNE7M7zVK?x_`dBSgPK zWY_~wF~OEZi9|~CSH8}Xi>#8G73!QLCAh58W+KMJJC81{60?&~BM_0t-u|VsPBxn* zW7viEKwBBTsn_A{g@1!wnJ8@&h&d>!qAe+j_$$Vk;OJq`hrjzEE8Wjtm)Z>h=*M25 zOgETOM9-8xuuZ&^@rLObtcz>%iWe%!uGV09nUZ*nxJAY%&KAYGY}U1WChFik7HIw% zZP$3Bx|TG_`~19XV7kfi2GaBEhKap&)Q<9`aPs#^!kMjtPb|+-fX66z3^E)iwyXK7 z8)_p<)O{|i&!qxtgBvWXx8*69WO$5zACl++1qa;)0zlXf`eKWl!0zV&I`8?sG)OD2Vy?reNN<{eK+_ za4M;Hh%&IszR%)&gpgRCP}yheQ+l#AS-GnY81M!kzhWxIR?PW`G3G?} z$d%J28uQIuK@QxzGMKU_;r8P0+oIjM+k)&lZ39i#(ntY)*B$fdJnQ3Hw3Lsi8z&V+ zZly2}(Uzpt2aOubRjttzqrvinBFH4jrN)f0hy)tj4__UTwN)#1fj3-&dC_Vh7}ri* zfJ=oqLMJ-_<#rwVyN}_a-rFBe2>U;;1(7UKH!$L??zTbbzP#bvyg7OQBGQklJ~DgP zd<1?RJ<}8lWwSL)`jM53iG+}y2`_yUvC!JkMpbZyb&50V3sR~u+lok zT0uFRS-yx@8q4fPRZ%KIpLp8R#;2%c&Ra4p(GWRT4)qLaPNxa&?8!LRVdOUZ)2vrh zBSx&kB%#Y4!+>~)<&c>D$O}!$o{<1AB$M7-^`h!eW;c(3J~ztoOgy6Ek8Pwu5Y`Xion zFl9fb!k2`3uHPAbd(D^IZmwR5d8D$495nN2`Ue&`W;M-nlb8T-OVKt|fHk zBpjX$a(IR6*-swdNk@#}G?k6F-~c{AE0EWoZ?H|ZpkBxqU<0NUtvubJtwJ1mHV%9v?GdDw; zAyXZiD}f0Zdt-cl9(P1la+vQ$Er0~v}gYJVwQazv zH#+Z%2CIfOf90fNMGos|{zf&N`c0@x0N`tkFv|_9af3~<0z@mnf*e;%r*Fbuwl-IW z{}B3=(mJ#iwLIPiUP`J3SoP~#)6v;aRXJ)A-pD2?_2_CZ#}SAZ<#v7&Vk6{*i(~|5 z9v^nC`T6o`CN*n%&9+bopj^r|E(|pul;|q6m7Tx+U|UMjWK8o-lBSgc3ZF=rP{|l9 zc&R$4+-UG6i}c==!;I#8aDIbAvgLuB66CQLRoTMu~jdw`fPlKy@AKYWS-xyZzPg&JRAa@m-H43*+ne!8B7)HkQY4 zIh}NL4Q79a-`x;I_^>s$Z4J4-Ngq=XNWQ>yAUCoe&SMAYowP>r_O}S=V+3=3&(O=h zNJDYNs*R3Y{WLmBHc?mFEeA4`0Y`_CN%?8qbDvG2m}kMAiqCv`_BK z_6a@n`$#w6Csr@e2YsMx8udNWtNt=kcqDZdWZ-lGA$?1PA*f4?X*)hjn{sSo8!bHz zb&lGdAgBx@iTNPK#T_wy`KvOIZvTWqSHb=gWUCKXAiB5ckQI`1KkPx{{%1R*F2)Oc z(9p@yG{fRSWE*M9cdbrO^)8vQ2U`H6M>V$gK*rz!&f%@3t*d-r3mSW>D;wYxOhUul zk~~&ip5B$mZ~-F1orsq<|1bc3Zpw6)Ws5;4)HilsN;1tx;N6)tuePw& z==OlmaN*ybM&-V`yt|;vDz(_+UZ0m&&9#{9O|?0I|4j1YCMW;fXm}YT$0%EZ5^YEI z4i9WV*JBmEU{qz5O{#bs`R1wU%W$qKx?bC|e-iS&d*Qm7S=l~bMT{~m3iZl+PIXq{ zn-c~|l)*|NWLM%ysfTV-oR0AJ3O>=uB-vpld{V|cWFhI~sx>ciV9sPkC*3i0Gg_9G!=4ar*-W?D9)?EFL1=;O+W8}WGdp8TT!Fgv z{HKD`W>t(`Cds_qliEzuE!r{ihwEv1l5o~iqlgjAyGBi)$%zNvl~fSlg@M=C{TE;V zQkH`zS8b&!ut(m)%4n2E6MB>p*4(oV>+PT51#I{OXs9j1vo>9I<4CL1kv1aurV*AFZ^w_qfVL*G2rG@D2 zrs87oV3#mf8^E5hd_b$IXfH6vHe&lm@7On~Nkcq~YtE!}ad~?5*?X*>y`o;6Q9lkk zmf%TYonZM`{vJg$`lt@MXsg%*&zZZ0uUSse8o=!=bfr&DV)9Y6$c!2$NHyYAQf*Rs zk{^?gl9E z5Im8wlAsvQ6C2?DyG@95gUXZ3?pPijug25g;#(esF_~3uCj3~94}b*L>N2GSk%Qst z=w|Z>UX$m!ZOd(xV*2xvWjN&c5BVEdVZ0wvmk)I+YxnyK%l~caR=7uNQ=+cnNTLZ@&M!I$Mj-r{!P=; z`C2)D=VmvK8@T5S9JZoRtN!S*D_oqOxyy!q6Zk|~4aT|*iRN)fL)c>-yycR>-is0X zKrko-iZw(f(!}dEa?hef5yl%p0-v-8#8CX8!W#n2KNyT--^3hq6r&`)5Y@>}e^4h- zlPiDT^zt}Ynk&x@F8R&=)k8j$=N{w9qUcIc&)Qo9u4Y(Ae@9tA`3oglxjj6c{^pN( zQH+Uds2=9WKjH#KBIwrQI%bbs`mP=7V>rs$KG4|}>dxl_k!}3ZSKeEen4Iswt96GGw`E6^5Ov)VyyY}@itlj&sao|>Sb5 zeY+#1EK(}iaYI~EaHQkh7Uh>DnzcfIKv8ygx1Dv`8N8a6m+AcTa-f;17RiEed>?RT zk=dAksmFYPMV1vIS(Qc6tUO+`1jRZ}tcDP? zt)=7B?yK2RcAd1+Y!$K5*ds=SD;EEqCMG6+OqPoj{&8Y5IqP(&@zq@=A7+X|JBRi4 zMv!czlMPz)gt-St2VZwDD=w_S>gRpc-g zUd*J3>bXeZ?Psjohe;z7k|d<*T21PA1i)AOi8iMRwTBSCd0ses{)Q`9o&p9rsKeLaiY zluBw{1r_IFKR76YCAfl&_S1*(yFW8HM^T()&p#6y%{(j7Qu56^ZJx1LnN`-RTwimdnuo*M8N1ISl+$C-%=HLG-s} zc99>IXRG#FEWqSV9@GFW$V8!{>=lSO%v@X*pz*7()xb>=yz{E$3VE;e)_Ok@A*~El zV$sYm=}uNlUxV~6e<6LtYli1!^X!Ii$L~j4e{sI$tq_A(OkGquC$+>Rw3NFObV2Z)3Rt~Jr{oYGnZaFZ^g5TDZlg;gaeIP} z!7;T{(9h7mv{s@piF{-35L=Ea%kOp;^j|b5ZC#xvD^^n#vPH=)lopYz1n?Kt;vZmJ z!FP>Gs7=W{sva+aO9S}jh0vBs+|(B6Jf7t4F^jO3su;M13I{2rd8PJjQe1JyBUJ5v zcT%>D?8^Kp-70bP8*rulxlm)SySQhG$Pz*bo@mb5bvpLAEp${?r^2!Wl*6d7+0Hs_ zGPaC~w0E!bf1qFLDM@}zso7i~(``)H)zRgcExT_2#!YOPtBVN5Hf5~Ll3f~rWZ(UsJtM?O*cA1_W0)&qz%{bDoA}{$S&-r;0iIkIjbY~ zaAqH45I&ALpP=9Vof4OapFB`+_PLDd-0hMqCQq08>6G+C;9R~}Ug_nm?hhdkK$xpI zgXl24{4jq(!gPr2bGtq+hyd3%Fg%nofK`psHMs}EFh@}sdWCd!5NMs)eZg`ZlS#O0 zru6b8#NClS(25tXqnl{|Ax@RvzEG!+esNW-VRxba(f`}hGoqci$U(g30i}2w9`&z= zb8XjQLGN!REzGx)mg~RSBaU{KCPvQx8)|TNf|Oi8KWgv{7^tu}pZq|BS&S<53fC2K4Fw6>M^s$R$}LD*sUxdy6Pf5YKDbVet;P!bw5Al-8I1Nr(`SAubX5^D9hk6$agWpF}T#Bdf{b9-F#2WVO*5N zp+5uGgADy7m!hAcFz{-sS0kM7O)qq*rC!>W@St~^OW@R1wr{ajyYZq5H!T?P0e+)a zaQ%IL@X_`hzp~vRH0yUblo`#g`LMC%9}P;TGt+I7qNcBSe&tLGL4zqZqB!Bfl%SUa z6-J_XLrnm*WA`34&mF+&e1sPCP9=deazrM=Pc4Bn(nV;X%HG^4%Afv4CI~&l!Sjzb z{rHZ3od0!Al{}oBO>F*mOFAJrz>gX-vs!7>+_G%BB(ljWh$252j1h;9p~xVA=9_`P z5KoFiz96_QsTK%B&>MSXEYh`|U5PjX1(+4b#1PufXRJ*uZ*KWdth1<0 zsAmgjT%bowLyNDv7bTUGy|g~N34I-?lqxOUtFpTLSV6?o?<7-UFy*`-BEUsrdANh} zBWkDt2SAcGHRiqz)x!iVoB~&t?$yn6b#T=SP6Ou8lW=B>=>@ik93LaBL56ub`>Uo!>0@O8?e)$t(sgy$I z6tk3nS@yFFBC#aFf?!d_3;%>wHR;A3f2SP?Na8~$r5C1N(>-ME@HOpv4B|Ty7%jAv zR}GJwsiJZ5@H+D$^Cwj#0XA_(m^COZl8y7Vv(k=iav1=%QgBOVzeAiw zaDzzdrxzj%sE^c9_uM5D;$A_7)Ln}BvBx^=)fO+${ou%B*u$(IzVr-gH3=zL6La;G zu0Kzy5CLyNGoKRtK=G0-w|tnwI)puPDOakRzG(}R9fl7#<|oQEX;E#yCWVg95 z;NzWbyF&wGg_k+_4x4=z1GUcn6JrdX4nOVGaAQ8#^Ga>aFvajQN{!+9rgO-dHP zIp@%&ebVg}IqnRWwZRTNxLds+gz2@~VU(HI=?Epw>?yiEdZ>MjajqlO>2KDxA>)cj z2|k%dhh%d8SijIo1~20*5YT1eZTDkN2rc^zWr!2`5}f<2f%M_$to*3?Ok>e9$X>AV z2jYmfAd)s|(h?|B(XYrIfl=Wa_lBvk9R1KaP{90-z{xKi+&8=dI$W0+qzX|ZovWGOotP+vvYR(o=jo?k1=oG?%;pSqxcU* zWVGVMw?z__XQ9mnP!hziHC`ChGD{k#SqEn*ph6l46PZVkm>JF^Q{p&0=MKy_6apts z`}%_y+Tl_dSP(;Ja&sih$>qBH;bG;4;75)jUoVqw^}ee=ciV;0#t09AOhB^Py7`NC z-m+ybq1>_OO+V*Z>dhk}QFKA8V?9Mc4WSpzj{6IWfFpF7l^au#r7&^BK2Ac7vCkCn{m0uuN93Ee&rXfl1NBY4NnO9lFUp zY++C1I;_{#OH#TeP2Dp?l4KOF8ub?m6zE@XOB5Aiu$E~QNBM@;r+A5mF2W1-c7>ex zHiB=WJ&|`6wDq*+xv8UNLVUy4uW1OT>ey~Xgj@MMpS@wQbHAh>ysYvdl-1YH@&+Q! z075(Qd4C!V`9Q9jI4 zSt{HJRvZec>vaL_brKhQQwbpQd4_Lmmr0@1GdUeU-QcC{{8o=@nwwf>+dIKFVzPriGNX4VjHCa zTbL9w{Y2V87c2ofX%`(48A+4~mYTiFFl!e{3K^C_k%{&QTsgOd0*95KmWN)P}m zTRr{`f7@=v#+z_&fKYkQT!mJn{*crj%ZJz#(+c?>cD&2Lo~FFAWy&UG*Op^pV`BR^I|g?T>4l5;b|5OQ@t*?_Slp`*~Y3`&RfKD^1uLezIW(cE-Dq2z%I zBi8bWsz0857`6e!ahet}1>`9cYyIa{pe53Kl?8|Qg2RGrx@AlvG3HAL-^9c^1GW;)vQt8IK+ zM>!IW*~682A~MDlyCukldMd;8P|JCZ&oNL(;HZgJ>ie1PlaInK7C@Jg{3kMKYui?e!b`(&?t6PTb5UPrW-6DVU%^@^E`*y-Fd(p|`+JH&MzfEq;kikdse ziFOiDWH(D< zyV7Rxt^D0_N{v?O53N$a2gu%1pxbeK;&ua`ZkgSic~$+zvt~|1Yb=UfKJW2F7wC^evlPf(*El+#}ZBy0d4kbVJsK- z05>;>?HZO(YBF&v5tNv_WcI@O@LKFl*VO?L(!BAd!KbkVzo;v@~3v`-816GG?P zY+H3ujC>5=Am3RIZDdT#0G5A6xe`vGCNq88ZC1aVXafJkUlcYmHE^+Z{*S->ol%-O znm9R0TYTr2w*N8Vs#s-5=^w*{Y}qp5GG)Yt1oLNsH7y~N@>Eghms|K*Sdt_u!&I}$ z+GSdFTpbz%KH+?B%Ncy;C`uW6oWI46(tk>r|5|-K6)?O0d_neghUUOa9BXHP*>vi; z={&jIGMn-92HvInCMJcyXwHTJ42FZp&Wxu+9Rx;1x(EcIQwPUQ@YEQQ`bbMy4q3hP zNFoq~Qd0=|xS-R}k1Im3;8s{BnS!iaHIMLx)aITl)+)?Yt#fov|Eh>}dv@o6R{tG>uHsy&jGmWN5+*wAik|78(b?jtysPHC#e+Bzz~V zS3eEXv7!Qn4uWi!FS3B?afdD*{fr9>B~&tc671fi--V}~E4un;Q|PzZRwk-azprM$4AesvUb5`S`(5x#5VJ~4%ET6&%GR$}muHV-5lTsCi_R|6KM(g2PCD@|yOpKluT zakH!1V7nKN)?6JmC-zJoA#ciFux8!)ajiY%K#RtEg$gm1#oKUKX_Ms^%hvKWi|B=~ zLbl-L)-=`bfhl`>m!^sRR{}cP`Oim-{7}oz4p@>Y(FF5FUEOfMwO!ft6YytF`iZRq zfFr{!&0Efqa{1k|bZ4KLox;&V@ZW$997;+Ld8Yle91he{BfjRhjFTFv&^YuBr^&Pe zswA|Bn$vtifycN8Lxr`D7!Kygd7CuQyWqf}Q_PM}cX~S1$-6xUD%-jrSi24sBTFNz(Fy{QL2AmNbaVggWOhP;UY4D>S zqKr!UggZ9Pl9Nh_H;qI`-WoH{ceXj?m8y==MGY`AOJ7l0Uu z)>M%?dtaz2rjn1SW3k+p`1vs&lwb%msw8R!5nLS;upDSxViY98IIbxnh{}mRfEp=9 zbrPl>HEJeN7J=KnB6?dwEA6YMs~chHNG?pJsEj#&iUubdf3JJwu=C(t?JpE6xMyhA3e}SRhunDC zn-~83*9=mADUsk^sCc%&&G1q5T^HR9$P#2DejaG`Ui*z1hI#h7dwpIXg)C{8s< z%^#@uQRAg-$z&fmnYc$Duw63_Zopx|n{Bv*9Xau{a)2%?H<6D>kYY7_)e>OFT<6TT z0A}MQLgXbC2uf`;67`mhlcUhtXd)Kbc$PMm=|V}h;*_%vCw4L6r>3Vi)lE5`8hkSg zNGmW-BAOO)(W((6*e_tW&I>Nt9B$xynx|sj^ux~?q?J@F$L4;rnm_xy8E*JYwO-02u9_@@W0_2@?B@1J{y~Q39N3NX^t7#`=34Wh)X~sU&uZWgS1Z09%_k|EjA4w_QqPdY`oIdv$dJZ;(!k)#U8L+|y~gCzn+6WmFt#d{OUuKHqh1-uX_p*Af8pFYkYvKPKBxyid4KHc}H` z*KcyY;=@wzXYR{`d{6RYPhapShXIV?0cg_?ahZ7do)Ot#mxgXYJYx}<%E1pX;zqHd zf!c(onm{~#!O$2`VIXezECAHVd|`vyP)Uyt^-075X@NZDBaQt<>trA3nY-Dayki4S zZ^j6CCmx1r46`4G9794j-WC0&R9(G7kskS>=y${j-2;(BuIZTLDmAyWTG~`0)Bxqk zd{NkDe9ug|ms@0A>JVmB-IDuse9h?z9nw!U6tr7t-Lri5H`?TjpV~8(gZWFq4Vru4 z!86bDB;3lpV%{rZ`3gtmcRH1hjj!loI9jN>6stN6A*ujt!~s!2Q+U1(EFQEQb(h4E z6VKuRouEH`G6+8Qv2C)K@^;ldIuMVXdDDu}-!7FS8~k^&+}e9EXgx~)4V4~o6P^52 z)a|`J-fOirL^oK}tqD@pqBZi_;7N43%{IQ{v&G9^Y^1?SesL`;Z(dt!nn9Oj5Odde%opv&t zxJ><~b#m+^KV&b?R#)fRi;eyqAJ_0(nL*61yPkJGt;gZxSHY#t>ATnEl-E%q$E16% zZdQfvhm5B((y4E3Hk6cBdwGdDy?i5CqBlCVHZr-rI$B#>Tbi4}Gcvyg_~2=6O9D-8 zY2|tKrNzbVR$h57R?Pe+gUU_il}ZaWu|Az#QO@};=|(L-RVf0AIW zq#pO+RfM7tdV`9lI6g;{qABNId`fG%U9Va^ravVT^)CklDcx)YJKeJdGpM{W1v8jg z@&N+mR?BPB=K1}kNwXk_pj44sd>&^;d!Z~P>O78emE@Qp@&8PyB^^4^2f7e)gekMv z2aZNvP@;%i{+_~>jK7*2wQc6nseT^n6St9KG#1~Y@$~zR_=AcO2hF5lCoH|M&c{vR zSp(GRVVl=T*m~dIA;HvYm8HOdCkW&&4M~UDd^H)`p__!4k+6b)yG0Zcek8OLw$C^K z3-BbLiG_%qX|ZYpXJ$(c@aa7b4-*IQkDF}=gZSV`*ljP|5mWuHSCcf$5qqhZTv&P?I$z^>}qP(q!Aku2yA5vu38d8x*q{6-1`%PrE_r0-9Qo?a#7Zbz#iGI7K<(@k^|i4QJ1H z4jx?{rZbgV!me2VT72@nBjucoT zUM9;Y%TCoDop?Q5fEQ35bCYk7!;gH*;t9t-QHLXGmUF;|vm365#X)6b2Njsyf1h9JW#x$;@x5Nx2$K$Z-O3txa%;OEbOn6xBzd4n4v)Va=sj5 z%rb#j7{_??Tjb8(Hac<^&s^V{yO-BL*uSUk2;X4xt%NC8SjO-3?;Lzld{gM5A=9AV z)DBu-Z8rRvXXwSVDH|dL-3FODWhfe1C_iF``F05e{dl(MmS|W%k-j)!7(ARkV?6r~ zF=o42y+VapxdZn;GnzZfGu<6oG-gQ7j7Zvgo7Am@jYxC2FpS@I;Jb%EyaJDBQC(q% zKlZ}TVu!>;i3t~OAgl@QYy1X|T~D{HOyaS*Bh}A}S#a9MYS{XV{R-|niEB*W%GPW! zP^NU(L<}>Uab<;)#H)rYbnqt|dOK(-DCnY==%d~y(1*{D{Eo1cqIV8*iMfx&J*%yh zx=+WHjt0q2m*pLx8=--UqfM6ZWjkev>W-*}_*$Y(bikH`#-Gn#!6_ zIA&kxn;XYI;eN9yvqztK-a113A%97in5CL5Z&#VsQ4=fyf&3MeKu70)(x^z_uw*RG zo2Pv&+81u*DjMO6>Mrr7vKE2CONqR6C0(*;@4FBM;jPIiuTuhQ-0&C)JIzo_k>TaS zN_hB;_G=JJJvGGpB?uGgSeKaix~AkNtYky4P7GDTW6{rW{}V9K)Cn^vBYKe*OmP!; zohJs=l-0sv5&pL6-bowk~(swtdRBZQHh8)m^r2+qTtZ zt4m$B?OQYNyfBA0E)g28a*{)a=%%f-?{F;++-Xs#5|7kSHTD*E9@$V ztE%7zX4A(L`n)FY8Y4pOnKC|Pf)j$iR#yP;V0+|Hki+D;t4I4BjkfdYliK9Gf6RYw z;3px$Ud5aTd`yq$N7*WOs!{X91hZZ;AJ9iQOH%p;v$R%OQum_h#rq9*{ve(++|24z zh2P;{-Z?u#rOqd0)D^_Ponv(Y9KMB9#?}nJdUX&r_rxF0%3__#8~ZwsyrSPmtWY27 z-54ZquV2t_W!*+%uwC=h-&_q~&nQer0(FL74to%&t^byl^C?wTaZ-IS9OssaQFP)1 zAov0o{?IRAcCf+PjMWSdmP42gysh|c9Ma&Q^?_+>>+-yrC8WR;*XmJ;>r9v*>=W}tgWG;WIt{~L8`gk8DP{dSdG z4SDM7g5ahMHYHHk*|mh9{AKh-qW7X+GEQybJt9A@RV{gaHUAva+=lSroK^NUJYEiL z?X6l9ABpd)9zzA^;FdZ$QQs#uD@hdcaN^;Q=AXlbHv511Meye`p>P4Y2nblEDEeZo}-$@g&L98Aih6tgLz--${eKTxymIipy0xSYgZZ zq^yyS4yNPTtPj-sM?R8@9Q1gtXPqv{$lb5i|C1yymwnGdfYV3nA-;5!Wl zD0fayn!B^grdE?q^}ba{-LIv*Z}+hZm_F9c$$cW!bx2DgJD&6|bBIcL@=}kQA1^Eh zXTEznqk)!!IcTl>ey?V;X8k<+C^DRA{F?T*j0wV`fflrLBQq!l7cbkAUE*6}WabyF zgpb+|tv=aWg0i}9kBL8ZCObYqHEycr5tpc-$|vdvaBsu#lXD@u_e1iL z{h>xMRS0a7KvW?VttrJFpX^5DC4Bv4cp6gNG6#8)7r7IxXfSNSp6)_6tZ4l>(D+0I zPhU)N!sKywaBusHdVE!yo5$20JAU8V_XcW{QmO!p*~ns8{2~bhjydnmA&=r zX9NSM9QYogYMDZ~kS#Qx`mt>AmeR3p@K$`fbJ%LQ1c5lEOz<%BS<}2DL+$>MFcE%e zlxC)heZ7#i80u?32eOJI9oQRz0z;JW@7Th4q}YmQ-`Z?@y3ia^_)7f37QMwDw~<-@ zT)B6fftmK_6YS!?{uaj5lLxyR++u*ZY2Mphm5cd7PA5=%rd)95hJ9+aGSNfjy>Ylc zoI0nGIT3sKmwX8h=6CbvhVO+ehFIR155h8iRuXZx^cW>rq5K4z_dvM#hRER=WR@THs%WELI9uYK9HN44Em2$#@k)hD zicqRPKV#yB;UlcsTL_}zCMK0T;eXHfu`y2(dfwm(v)IBbh|#R>`2cot{m7}8_X&oD zr@94PkMCl%d3FsC4pil=#{3uv^+)pvxfwmPUr)T)T|GcZVD$wVj$mjkjDs`5cm8N! zXVq2CvL;gWGpPI4;9j;2&hS*o+LNp&C5Ac=OXx*W5y6Z^az)^?G0)!_iAfjH5wiSE zD(F}hQZB#tF5iEx@0sS+dP70DbZ*<=5X^)Pxo^8aKzOzuyc2rq=<0-k;Y_ID1>9^v z+)nc36}?>jen*1%OX3R*KRASj${u$gZ$27Hpcj=95kK^aLzxhW6jj_$w6}%#1*$5D zG1H_vYFrCSwrRqYw*9<}OYAOQT)u%9lC`$IjZV<4`9Sc;j{Qv_6+uHrYifK&On4V_7yMil!0Yv55z@dFyD{U@Sy>|vTX=P_( zRm<2xj*Z}B30VAu@0e+}at*y?wXTz|rPalwo?4ZZc>hS0Ky6~mi@kv#?xP2a;yt?5=(-CqvP_3&$KdjB7Ku;# z`GLE*jW1QJB5d&E?IJO?1+!Q8HQMGvv^RuFoi=mM4+^tOqvX%X&viB%Ko2o-v4~~J z267ui;gsW?J=qS=D*@*xJvAy3IOop5bEvfR4MZC>9Y4Z$rGI|EHNNZ7KX;Ix{xSvm z-)Cau-xuTm|7`4kUdXvd_d^E=po(76ELfq5OgxIt3aqDy#zBfIy-5<3gpn{Ce`-ha z<;6y@{Bgqw?c~h*&j{FozQCh=`Lv-5Iw!KdSt;%GDOq%=(V!dJ-}|}|0o5G2kJj6{ z`jCSPs$9Fe8O(+qALZiJ$WtR=<@GvsdM)IJ`7XrBfW0iyYE#Vy^e@zbysg*B5Z_kSL6<)vqoaH zQ{!9!*{e9UZo^h+qZ`T@LfVwAEwc&+9{C8c%oj41q#hyn<&zA9IIur~V|{mmu`n5W z8)-Ou$YgjQ*PMIqHhZ_9E?(uoK0XM5aQkarcp}WT^7b^FC#^i>#8LGZ9puDuXUYas z7caX)V5U6uY-L5Wl%)j$qRkR;7@3T*N64YK_!`Fw=>CAwe~2loI1<>DZW&sb7Q)X;6E08&$h! z2=c1i4UOO{R4TmkTz+o9n`}+%d%blR6P;5{`qjtxlN$~I%tMMDCY`~e{+mRF!rj5( z3ywv)P_PUUqREu)TioPkg&5RKjY6z%pRxQPQ{#GNMTPag^S8(8l{!{WGNs2U1JA-O zq02VeYcArhTAS;v3);k(&6ayCH8SXN@r;1NQeJ*y^NHM+zOd;?t&c!Hq^SR_w6twGV8dl>j zjS+Zc&Yp7cYj&c1y3IxQ%*kWiYypvoh(k8g`HrY<_Bi-r%m-@SLfy-6mobxkWHxyS z>TtM2M4;Uqqy|+8Q++VcEq$PwomV1D4UzNA*Tgkg9#Gpz#~&iPf|Czx!J?qss?e|3 z4gTua75-P{2X7w9eeK3~GE0ip-D;%%gTi)8bR~Ez@)$gpuS~jZs`CrO5SR-Xy7bkA z89fr~mY}u4A$|r1$fe-;T{yJh#9Ime1iRu8eo?uY9@yqAU3P!rx~SsP;LTBL zeoMK(!;(Zt8313 z3)V)q_%eflKW?BnMZa}6E0c7t!$-mC$qt44OME5F(6B$E8w*TUN-h}0dOiXI+TH zYFrr&k1(yO(|J0vP|{22@Z}bxm@7BkjO)f)&^fv|?_JX+s)1*|7X7HH(W?b3QZ3!V|~m?8}uJsF>NvE4@fik zjyyh+U*tt`g6v>k9ub88a;ySvS1QawGn7}aaR**$rJA=a#eUT~ngUbJ%V=qsFIekLbv!YkqjTG{_$F;$w19$(ivIs*1>?2ka%uMOx@B9`LD zhm~)z@u4x*zcM1WhiX)!U{qOjJHt1xs{G1S?rYe)L)ntUu^-(o_dfqZu)}W(X%Uu| zN*qI@&R2fB#Jh|Mi+eMrZDtbNvYD3|v0Kx>E#Ss;Be*T$@DC!2A|mb%d}TTN3J+c= zu@1gTOXFYy972S+=C;#~)Z{Swr0VI5&}WYzH22un_Yg5o%f9fvV(`6!{C<(ZigQ2`wso)cj z9O12k)15^Wuv#rHpe*k5#4vb%c znP+Gjr<-p%01d<+^yrSoG?}F=eI8X;?=Fo2a~HUiJ>L!oE#9tXRp!adg-b9D;(6$E zeW0tH$US04zTX$OxM&X+2ip>KdFM?iG_fgOD-qB|uFng8*#Z5jgqGY=zLU?4!OlO#~YBTB9b9#~H@nqQ#5 z6bV));d?IJTVBC+79>rGuy1JgxPLy$dA7;_^^L)02m}XLjFR*qH`eI~+eJo(7D`LH z(W%lGnGK+Vk_3kyF*zpgO=1MxMg?hxe3}}YI>dVs8l}5eWjYu4=w6MWK09+05 zGdpa#$awd>Q|@aZa*z{5F3xy3n@E4YT9%TmMo0jxW59p0bI?&S}M+ z&^NG%rf7h*m9~p#b19|`wO5OMY-=^XT+=yrfGNpl<&~~FGsx_`IaFn+sEgF$hgOa~oAVAiu^a$jHcqkE=dj`ze z=axsfrzzh6VGD0x#6Ff=t%+VTiq!n6^gv*uIUD<9fOhvR;al5kcY${uunn}-!74<7 zmP^3cl-kyN(QY!!Z-^PY-OUkh=3ZWk6>le$_Q&xk4cgH{?i)C%2RM@pX5Q{jdSlo! zVau5v44cQX5|zQlQDt;dCg)oM0B<=P1CR!W%!^m$!{pKx;bn9DePJjWBX)q!`$;0K zqJIIyD#aK;#-3&Nf=&IhtbV|?ZGYHSphp~6th`p2rkw&((%kBV7<{siEOU7AxJj+FuRdDu$ zcmTW8usU_u!r)#jg|J=Gt{##7;uf4A5cdt6Y02}f(d2)z~ z)CH~gVAOwBLk$ZiIOn}NzDjvfw(w$u|BdCBI#)3xB-Ot?nz?iR38ayCm48M=_#9r7 zw8%pwQ<9mbEs5~_>pN3~#+Er~Q86J+2TDXM6umCbukd-X6pRIr5tF?VauT8jW> zY^#)log>jtJs2s3xoiPB7~8#1ZMv>Zx0}H58k-@H2huNyw~wsl0B8j)H5)H9c7y&i zp8^0;rKbxC1eEZ-#Qxvz)Xv$((8lK9I>BspPajluysw^f#t9P;OUis43mmEzX+lk* zc4T-Ms9_687GR+~QS#0~vxK#DSGN=a-m(@eZTqw2<+lN9>R~gK2)3;sT4%nI%Y|0m zX9SPR!>?~s=j5H4WMqeTW8QaLZ=1bWS5I3xZ&$(ypc=tHrv+hX@s)VG(tc!yvLM7n zshN=C#v={X1r;)xn0Pow_1eMhkn!{;x$BJ#PIz)m585&%cmzk;btQzZAN_^zis;n? z?6I~bN?s;7vg_dtoTc4A5Ow*Rb}No#UYl)sN|RmoYo}k^cKLXd8F`44?RrokkPvN5 ztUrx;U~B;jbE_qGd3n0j2i}A{enJvJ?gSF~NQj~EP5vM-w4@;QQ5n(Npic}XNW6B0 zq9F4T%6kp7qGhd0vpQcz+nMk8GOAmbz8Bt4@GtewGr6_>Xj>ge)SyfY}nu>Y!a@HoIx(StD zx`!>RT&}tpBL%nOF%7XIFW?n1AP*xthCMzhrU6G!U6?m4!CPWTvn#Yaoi_95CT2!L z|B=5zeRW30&ANGN>J9#GtCm&3SF6n4TqDz<-{@ZXkrkRDCpV$DwCtI^e&3i1A{Ar&JZtS^c+lyPa6 z%JJr42S_;eFC#M~bdtQePhOU32WDiZ4@H&af)z#$Y|hnQNb)8(3?1Ad>5uaZ1z zU~!jt3XUI@gpWb8tWTyH7DGvKvzYfqNIy3P{9vpwz_C-QL&`+8Io$F5PS-@YQJoEO z17D9P(+sXajWSH_8&C?fn>rTLX+(?KiwX#JNV)xE0!Q@>Tid$V2#r4y6fkph?YZ>^ z(o^q(0*P->3?I0cELXJn(N|#qTm6 zAPIL~n)m!50;*?5=MOOc4Wk;w(0c$(!e?vpV23S|n|Y7?nyc8)fD8t-KI&nTklH&BzqQ}D(1gH3P+5zGUzIjT~x`;e8JH=86&5&l-DP% z)F+Et(h|GJ?rMy-Zrf>Rv@<3^OrCJ1xv_N*_@-K5=)-jP(}h1Rts44H&ou8!G_C1E zhTfUDASJ2vu!4@j58{NN;78i?6__xR75QEDC4JN{>RmgcNrn-EOpEOcyR<8FS@RB@ zH!R7J=`KK^u06eeI|X@}KvQmdKE3AmAy8 zM4IIvde#e4O(iwag`UL5yQo>6&7^=D4yE-Eo9$9R2hR} zn;Z9i-d=R-xZl4@?s%8|m1M`$J6lW1r0Y)+8q$}Vn4qyR1jqTjGH;@Z!2KiGun2~x zaiEfzVT<|_b6t}~XPeflAm8hvCHP3Bp*tl{^y_e{Jsn@w+KP{7}bH_s=1S2E1sj=18a39*Ag~lbkT^_OQuYQey=b zW^{0xlQ@O$^cSxUZ8l(Mspg8z0cL*?yH4;X2}TdN)uN31A%$3$a=4;{S@h#Y(~i%) zc=K7Ggl=&2hYVic*W65gpSPE70pU;FN@3k?BYdNDKv6wlsBAF^);qiqI zhklsX4TaWiC%VbnZ|yqL+Pcc;(#&E*{+Rx&<&R{uTYCn^OD|mAk4%Q7gbbgMnZwE{ zy7QMK%jIjU@ye?0; z;0--&xVeD}m_hq9A8a}c9WkI2YKj8t!Mkk!o%AQ?|CCBL9}n570}OmZ(w)YI6#QS&p<={tcek*D{CPR%eVA1WBGUXf z%gO2vL7iVDr1$!LAW)1@H>GoIl=&yyZ7=*9;wrOYQ}O}u>h}4FWL?N2ivURlUi11- zl{G0fo`9?$iAEN<4kxa#9e0SZPqa{pw?K=tdN5tRc7HDX-~Ta6_+#s9W&d`6PB7dF*G@|!Mc}i zc=9&T+edI(@la}QU2An#wlkJ&7RmTEMhyC_A8hWM54?s1WldCFuBmT5*I3K9=1aj= z6V@93P-lUou`xmB!ATp0(We$?)p*oQs;(Kku15~q9`-LSl{(Efm&@%(zj?aK2;5}P z{6<@-3^k^5FCDT@Z%XABEcuPoumYkiD&)-8z2Q}HO9OVEU3WM;V^$5r4q>h^m73XF z5!hZ7SCjfxDcXyj(({vg8FU(m2_}36L_yR>fnW)u=`1t@mPa76`2@%8v@2@$N@TE` z)kYhGY1jD;B9V=Dv1>BZhR9IJmB?X9Wj99f@MvJ2Fim*R`rsRilvz_3n!nPFLmj({EP!@CGkY5R*Y_dSO{qto~WerlG}DMw9k+n}pk z*nL~7R2gB{_9=zpqX|*vkU-dx)(j+83uvYGP?K{hr*j2pQsfXn<_As6z%-z+wFLqI zMhTkG>2M}#BLIOZ(ya1y8#W<+uUo@(43=^4@?CX{-hAuaJki(_A(uXD(>`lzuM~M;3XA48ZEN@HRV{1nvt?CV)t;|*dow0Ue2`B*iA&!rI`fZQ=b28= z_dxF}iUQ8}nq0SA4NK@^EQ%=)OY;3fC<$goJ&Kp|APQ@qVbS-MtJQBc)^aO8mYFsbhafeRKdHPW&s^&;%>v zlTz`YE}CuQ@_X&mqm{+{!h2r)fPGeM_Ge4RRYQkrma`&G<>RW<>S(?#LJ}O-t)d$< zf}b0svP^Zu@)MqwEV^Fb_j zPYYs~vmEC~cOIE6Nc^@b@nyL!w5o?nQ!$mGq(Pa|1-MD}K0si<&}eag=}WLSDO zE4+eA~!J(K}605x&4 zT72P7J^)Y)b(3g2MZ@1bv%o1ggwU4Yb!DhQ=uu-;vX+Ix8>#y6wgNKuobvrPNx?$3 zI{BbX<=Y-cBtvY&#MpGTgOLYU4W+csqWZx!=AVMb)Z;8%#1*x_(-)teF>45TCRwi1 z)Nn>hy3_lo44n-4A@=L2gI$yXCK0lPmMuldhLxR8aI;VrHIS{Dk}yp= zwjhB6v@0DN=Hnm~3t>`CtnPzvA*Kumfn5OLg&-m&fObRD};c}Hf?n&mS< z%$wztc%kjWjCf-?+q(bZh9k~(gs?i4`XVfqMXvPVkUWfm4+EBF(nOkg!}4u)6I)JT zU6IXqQk?p1a2(bz^S;6ZH3Wy9!JvbiSr7%c$#G1eK2^=~z1WX+VW)CPD#G~)13~pX zErO(>x$J_4qu-)lNlZkLj2}y$OiKn0ad5Imu5p-2dnt)(YI|b7rJ3TBUQ8FB8=&ym50*ibd2NAbj z;JA&hJ$AJlldM+tO;Yl3rBOFiP8fDdF?t(`gkRpmT9inR@uX{bThYNmxx-LN5K8h0 ztS%w*;V%b`%;-NARbNXn9he&AO4$rvmkB#;aaOx?Wk|yBCmN{oMTK&E)`s&APR<-5 z#;_e75z;LJ)gBG~h<^`SGmw<$Z3p`KG|I@7Pd)sTJnouZ1hRvm3}V+#lPGk4b&A#Y z4VSNi8(R1z7-t=L^%;*;iMTIAjrXl;h106hFrR{n9o8vlz?+*a1P{rEZ2ie{luQs} zr6t746>eoqiO5)^y;4H%2~&FT*Qc*9_oC2$+&syHWsA=rn3B~4#QEW zf4GT3i_@)f(Fj}gAZj`7205M8!B&HhmbgyZB& z+COyAVNxql#DwfP;H48Yc+Y~ChV6b9auLnfXXvpjr<~lQ@>VbCpQvWz=lyVf1??_c zAo3C^otZD@(v?X)UX*@w?TF|F8KF>l7%!Dzu+hksSA^akEkx8QD(V(lK+HBCw6C}2onVExW)f$ zncm*HI(_H;jF@)6eu}Tln!t?ynRkcqBA5MitIM@L^(4_Ke}vy7c%$w{(`&7Rn=u>oDM+Z^RUYcbSOPwT(ONyq76R>$V6_M_UP4vs=__I#io{{((| zy5=k=oVr-Qt$FImP~+&sN8rf2UH*vRMpwohPc@9?id17La4weIfBNa>1Djy+1=ugn z@}Zs;eFY1OC}WBDxDF=i=On_33(jWE-QYV)HbQ^VM!n>Ci9_W0Zofz7!m>do@KH;S z4k}FqEAU2)b%B_B-QcPnM5Zh=dQ+4|DJoJwo?)f2nWBuZE@^>a(gP~ObzMuyNJTgJFUPcH`%9UFA(P23iaKgo0)CI!SZ>35LpFaD7 z)C2sW$ltSEYNW%%j8F;yK{iHI2Q^}coF@LX`=EvxZb*_O;2Z0Z5 z7 zlccxmCfCI;_^awp|G748%Wx%?t9Sh8!V9Y(9$B?9R`G)Nd&snX1j+VpuQ@GGk=y(W zK|<$O`Cad`Y4#W3GKXgs%lZduAd1t1<7LwG4*zaStE*S)XXPFDyKdgiaVXG2)LvDn zf}eQ_S(&2!H0Mq1Yt&WpM1!7b#yt_ie7naOfX129_E=)beKj|p1VW9q>>+e$3@G$K zrB%i_TT1DHjOf7IQ8)Wu4#K%ZSCDGMP7Ab|Kvjq7*~@ewPm~h_-8d4jmNH<&mNZC@CI zKxG5O08|@<4(6IEC@L-lcrrvix&_Dj4tBvl=8A}2UX|)~v#V$L22U}UHk`B-1MF(t zU6aVJWR!>Y0@4m0UA%Sq9B5;4hZvsOu=>L`IU4#3r_t}os|vSDVMA??h>QJ1FD1vR z*@rclvfD!Iqoxh>VP+?b9TVH8g@KjYR@rRWQy44A`f6doIi+8VTP~pa%`(Oa@5?=h z8>YxNvA##a3D0)^P|2|+0~f|UsAJV=q(S>eq-dehQ+T>*Q@qN zU8@kdpU5gGk%ozt?%c8oM6neA?GuSsOfU_b1U)uiEP8eRn~>M$p*R z43nSZs@^ahO78s zulbK@@{3=2=@^yZ)DuIC$ki;`2WNbD_#`LOHN9iMsrgzt-T<8aeh z(oXrqI$Kgt6)Icu=?11NWs>{)_ed1wh>)wv6RYNUA-C&bejw{cBE_5Wzeo!AHdTd+ z)d(_IKN7z^n|As~3XS=cCB_TgM7rK;X586re`{~Foml$aKs zb!4Pe7hEP|370EWwn$HKPM!kL94UPZ1%8B^e5fB+=Iw^6=?5n3tZGYjov83CLB&OQ++p)WCMeshCv_9-~G9C_2x`LxTDjUcW$l6e!6-&a^fM3oP9*g(H zmCk0nGt1UMdU#pfg1G0um5|sc|KO<+qU1E4iBF~RvN*+`7uNHH^gu{?nw2DSCjig% zI@ymKZSK=PhHJa(jW&xeApv&JcfSmNJ4uQ|pY=Lcc>=J|{>5Ug3@x#R_b@55xFgfs za^ANzWdD$ZYtFs$d7+oiw0ZmPk2&l|< zc8()wfiJx@EGpQT zG$8iLkQZ-086doF1R zh<#9cz_vRsJdoXbD=QgOtpm}cFAJX8c}>Jew;PQJSXSb^;wlC zxXLHTS|!GZ-VK_4wV<9bk4RUmlsByzW_^b>)$6R+jQ}^wco1nMA`9Lncs;&QGp!`5Tx#aXXU?}5_RrtUY zx(EMzDhl-a^y^f5yfFLMnOO#u)l69&4M?|ne|2EV>zQ}4JQCBel?~2I4?D|>L$%H(peOOII!U}i z-j)*h1rODe9{0`xmhG;`AKqw1p0_KhEIU8)DoGnEn9wAhXPaxO_(jNSij~J5m$P*$ z9Mt(t;eV}2+i|kjQpBFcNb7_(VbuF<;RQB~R~p>2*Lg>a&7DEEuq*I%Ls4{zHeUDq z+M0&YhEn^C*9-B4Q7HJ$xj)dORCXPK+)ZtLOa0o&)Sl+f(Y{p*68$-#yagW5^HQnQ z0pWpoQpxg8<&gx9im(>=x6v#&RbQ7^AsjxeSDA? zi4MEJUC~ByG!PiBjq7$pK&FA^5 z=Y@dtQnuy%IfsaR`TVP0q^3mixl&J-3!$H!ua#{A>0Z1JdLq#d4UV9nlYm641ZHl zH6mK~iI6lR3OUEVL}Z5{ONZ_6{Nk%Bv03ag<1HVN?R%w2^aR5@E>6(r>}IoMl$wRF zWr-DItN*k7T$NTT8B)+23c?171sADhjInb2Xb>GhFYGC&3{b>huvLlaS4O z^{j5q+b5H?Z)yuy%AByaVl2yj9cnalY1sMQ zXI#e%*CLajxGxP!K6xf9RD2pMHOfAa1d^Lr6kE`IBpxOiGXfNcoQ*FI6wsNtLD!T+ zC4r2q>5qz0f}UY^RY#1^0*FPO*Zp-U1h9U|qWjwqJaDB(pZ`<`U-xo7+JB$zvwV}^ z2>$0&Q5k#l|Er7*PPG1ycj4BGz zg&`d*?nUi1Q!OB>{V@T$A;)8@h;*Rb1{xk_8X<34L`s}xkH-rQZvjM`jI=jaJRGRg zeEcjYChf-78|RLrao%4HyZBfnAx5KaE~@Sx+o-2MLJ>j-6uDb!U`odj*=)0k)K75l zo^)8-iz{_k7-_qy{Ko~N#B`n@o#A22YbKiA>0f3k=p-B~XX=`Ug>jl$e7>I=hph0&AK z?ya;(NaKY_!od=tFUcGU5Kwt!c9EPUQLi;JDCT*{90O@Wc>b| zI;&GIY$JlQW^9?R$-OEUG|3sp+hn+TL(YK?S@ZW<4PQa}=IcUAn_wW3d!r#$B}n08 z*&lf(YN21NDJ74DqwV`l`RX(4zJ<(E4D}N0@QaE-hnfdPDku~@yhb^AeZL73RgovX z6=e>!`&e^l@1WA5h!}}PwwL*Gjg!LbC5g0|qb8H$^S{eGs%cc?4vTyVFW=s6KtfW? z@&Xm+E(uz(qDbwDvRQI9DdB<2sW}FYK9sg*f%-i*>*n{t-_wXvg~N7gM|a91B!x|K zyLbJ~6!!JZpZ`#HpCB8g#Q*~VU47Rp$NyZb3WhEgg3ivSwnjGJgi0BEV?!H}Z@QF| zrO`Kx*52;FR#J-V-;`oR-pr!t>bYf)UYcixN=(FUR6$fhN@~i09^3WeP3*)D*`*mJ z1u%klAbzQ=P4s%|FnVTZv%|@(HDB+ap5S#cFSJUSGkyI*Y>9Lwx|0lTs%uhoCW(f1 zi+|a9;vDPfh3nS<7m~wqTM6+pEm(&z-Ll;lFH!w#(Uk#2>Iv~2Hu}lITn7hnOny`~ z*Vj=r<&Nwpq^@g5m`u&QTBRoK*}plAuHg$L$~NO#wF0!*r0OfcS%)k0A??uY*@B^C zJe9WdU(w){rTIf<;rwJt^_35^d<A@$FqEZW6kwyfAo2x0T$Ye2MZox6Z7<%Qbu$}}u{rtE+h2M+Z}T4I zxF1cwJ(Uvp!T#mogWkhb(?SxD4_#tV(Sc8N4Gu*{Fh#})Pvb^ef%jrlnG*&Ie+J5 zsly5oo?1((um&lLDxn(DkYtk`My>lgKTp3Y4?hTQ4_`YNOFtjF-FUY#d#(EQd(rfz zB8z%Vi;?x)ZM$3c>yc5H8KBvSevnWNdCbAj?QCac)6-K~Xz@EZp}~N9q)5*Ufjz3C z6kkOeI{3H(^VO8hKDrVjy2DXd;5wr4nb`19yJi0DO@607MSx+7F$ zz3F7sl8JV@@sM$6`#JmSilqI%Bs)}Py2eFT;TjcG5?8$zwV60b(_5A>b#uk~7U^bO z>y|6SCrP2IGST(8HFuX|XQUXPLt2gL_hm|uj1Ws`O2VW>SyL^uXkl>Zvkcpi?@!F7 z%svLoT@{R#XrIh^*dE~$YhMwC+b7JE09NAS47kT%Ew zD!XjxA@1+KOAyu`H2z#h+pGm!lG>WI0v745l+Fd><3dh{ATq%h?JSdEt zu%J*zfFUx%Tx&0DS5WSbE)vwZSoAGT=;W#(DoiL($BcK;U*w`xA&kheyMLI673HCb7fGkp{_vdV2uo;vSoAH z9BuLM#Vzwt#rJH>58=KXa#O;*)_N{$>l7`umacQ0g$pI3iW4=L--O;Wiq0zy7OKp`j2r^y3`7X!?sq9rr5B{41BkBr1fEd1#Q3 z-dXc2RSb4U>FvpVhlQCIzQ-hs=8420z=7F2F(^xD;^RXgpjlh8S6*xCP#Gj2+Q0bAg?XARw3dnlQ*Lz3vk}m`HXmCgN=?bIL{T zi}Ds-xn|P)dxhraT@XY$ZQ&^%x8y!o+?n#+>+dZ1c{hYwNTNRke@3enT(a@}V*X{! z81+{Jc2UR;+Zcbc6cUlafh4DFKwp>;M}8SGD+YnW3Q_)*9Z_pny_z+MeYQmz?r%EVaN0d!NE*FVPq&U@vo{ef6wkMIDEWLbDs zz91$($XbGnQ?4WHjB~4xgPgKZts{p|g1B{-4##}#c5aL5C6_RJ_(*5>85B1}U!_<``}q-97Q7~u)(&lsb(WT^(*n7H%33%@_b zO5(?-v??s??33b19xiB7t_YT!q8!qAzN1#RD@3;kYAli%kazt#YN7}MhVu=ljuz27 z1`<+g8oVwy57&$`CiHeaM)tz(OSt4E# zJ@P6E*e504oUw~RD(=9WP8QdW^6wRdFbKII!GAWecJ(?{`EzTR@?j!3g?$@LLCt;U={>!9z7DU!(1Jq zqEwdx5q?W1Ncm7mXP8MFwAr?nw5$H%cb>Q><9j{Tk2RY9ngGvaJgWXx^r!ywk{ph- zs2PFto4@IIwBh{oXe;yMZJYlS?3%a-CJ#js90hoh5W5d^OMwCFmpryHFr|mG+*ZP$ zqyS5BW@s}|3xUO0PR<^{a2M(gkP5BDGxvkWkPudSV*TMRK5Qm4?~VuqVAOerffRt$HGAvp;M++Iq$E6alB z;ykBr-eZ6v_H^1Wip56Czj&=`mb^TsX|FPN#-gnlP03AkiJDM=?y|LzER1M93R4sC z*HT(;EV=*F*>!+Z{r!KG?6ODMGvkt3viG=@kQJHNMYd}bS4KrrHf4`&*(0m0R5Hqz zEk)r=sFeS?MZRvn<@Z0&bDw)XkMnw+_xqgp=W{;ioX`6;G-P9N%wfoYJ$-m$L#MC% z^sH?tSzA|WWP(cN3({~_*X$l{M*;1V{l$;T6b){#l4pswDTid26HaXgKed}13YIP= zJRvA3nmx{}R$Lr&S4!kWU3`~dxM}>VXWu6Xd(VP}z1->h&f%82eXD_TuTs@=c;l0T z|LHmWKJ+?7hkY=YM>t}zvb4|lV;!ARMtWFp!E^J=Asu9w&kVF*i{T#}sY++-qnVh! z5TQ|=>)+vutf{&qB+LO9^jm#rD7E5+tcorr^Fn5Xb0B;)f^$7Ev#}G_`r==ea294V z--v4LwjswWlSq9ba6i?IXr8M_VEGQ$H%hCqJTFQ3+1B9tmxDUhnNU%dy4+zbqYJ|o z3!N{b?A@{;cG2~nb-`|z;gEDL5ffF@oc3`R{fGi)0wtMqEkw4tRX3t;LVS3-zAmg^ zgL7Z{hmdPSz9oA@t>tZ1<|Khn&Lp=_!Q=@a?k+t~H&3jN?dr(}7s;{L+jiKY57?WsFBfW^mu6a03_^VKrdK=9egXw@!nzZ3TbYc*osyQNoCXPYoFS<&Nr97MrQCOK(gO8 z;0@iqRTJy4-RH)PJld5`AJN}n?5r^-enKrHQOR;z>UMfm+e8~4ZL5k>oXMiYq12Bx4eVQv0jFgp_zC#``sjZpywYqISMP}VZ@!~1Mf$!x|opj%mQ98JnSk@`~ zPmmyuPZKtZOnEC!1y!?`TYRsZ!II;d!iln}%e}bk5qIiUADERr*K$3dekgHV9TtBX zi5q!J!6Zgd#cLxRmZN^J`o@Zv{+p+<_#8^nvY)44Hw_2i@?R&5n^q33fpOnDg1nPQ z_r<$hURl~OketX|Tdbvf_7=3x^rSFJtEp@tuDpVB&uq)qW;xUQ7mmkr-@eZwa$l+? zoKk``Vz@TH#>jMce*8>@FZ+@BEUdYa_K0i|{*;j9MW3K%pnM*T;@>|o@lMhgLrpZP5aol(z>g;b4}|e$U~Fn zGL%(}p%Jsl4LxE!VW_Y4T>e}W4e#~F03H_^R!Q)kpJG{lO!@I4{mFo^V#ayHh_5~o zB$O71gcE(G@6xv);#Ky?e(Ed}^O+Ho(t=93T9T3TnEY(OVf_dR-gY@jj+iJSY?q|6prBv(S9A4k=2fNZz!W@S=B@~b?TJRTuBQq448@juN#Y=3q=^VCF>Z}n6wICJ<^^Kn8C;mK zZYiFSN#Z$?NDGV7(#}q2tAZAtE63icK-MY>UQu4MWlGIbJ$AF8Zt-jV;@7P5MPI>% zPWvO!t%1+s>-A%`;0^o8Ezeaa4DMwI8ooQrJ;ax@Qt*6XONWw)dPwOPI9@u*EG&844*1~EoZ2qsAe~M>d`;Bc_CWY zMoDKEmDh-}k9d6*<0g@aQmsnrM1H9IcKYZs)><)d92{|0Hh8?~XbF)7U+UmP@Pw_6geVB?7N$4J4*E0z3EO&5kRS(EE zv92(+e5WxLXMN{h;-|8@!Q#0q247hb^3R%*k3MuMO5*L}$0D#5P*N$aHd54C+=_RToYXTyewugOaDmGsCvb4H1s=@gkfVnzTCWKMa-Mm1v4Wq!t-JIrbV&EWwKDe ze#kJpOq#iRlFz%5#6Fio9IUlKnQ#X&DY8Ux#<-WqxAac-y%U_L+EZZ4Rg5*yNg`f< zSZn&uio@zanUCPqX1l4W&B!;UWs#P7B^|4WwoCxQXl|44n^cBNqu=3Vl*ltAqsUQO z9q_@nD0zq0O8r`coEm>9+|rA3HL#l}X;0##>SJS$cVavOZVCpSGf4mUU1( zWaRCUYc^9QbG9=vpWo%xP}CMFnMb{reA`K7tT(t5DM)d9l}jVPY>qoRzT zE3m-p#=i=$9x*CB`AL>SY}u3agYFl#uULNen#&44H;!L@I{RI=PlWxG8J((f)ma7A z@jLvQ>?Nx`n?3ChRG#HqE3MXP8*o3!Qq`+t8EMt_p)oeKHqPusBxPn!#?R??-=e3e zo73WNs_IZF`WLigre=|`aS2^> zN1zn!7k&Dh28t%VpJ%**&E!eAcB5oLjQFFcJQj*URMia%Ya3@q1UQ18=oWMM6`I}iT_&L1gl?*~6nU4q4Z0`H<5yDp(HeZ+RGf9`mM&= zn-qRp%i!g$R;i1d1aMZ{IewNjE@p2+Z{`x{*xL*x$?WV~{BjJpsP&C&JK0HLoyf z`0z^v&fBQSa!I7FU~9MaQ%e|?RP>sM^2PL!mE^Q1Ig_4M$5BRfi72oMYu6Ke?wmDX z@0a%-V|z}b23K=ye(W+fG#w|jJUnT{=KR5jfuq!RX}<1irTDw(${<&}dWQu4;EuE< z@3u4dBkQaCHHM&;cE0z50_V!(vJ1_V)A8?C#eJuLkt!98Z%|Bgzidc0j|z(&o)TCzYlrgZA zC3@i>L!&Gw_~7`>puB97I2lK)lESZQqVXc_8T^G2O#VHhO?IC$g zOYhXJ7)~C<8l|Xrftka@QuowScM{K&0zskoU$Aw~vIRVRF9TEQ4*3=_5)98B`=t8(N%ZuWqmwlW zllAzq=E5_5!sKDXam@w`ZD(nl%LAPxQuEtDcKPqu9LPJvNIITawU#c^PQ2HmZgs)r zH^+gRwZ?0)8IFQgU)+p@0Iqb^tcEoqcB@zhfz_FaOM&_d<|jnU>q5nSKa<@%9|dje zIupcg1!tRiMP4X=oG<7s4|AW&^-Cw4FL9OuI$t zxjc*y;Uw!G7a|jz>E*2+PlR(CemWebS7m-&*CDwnmxbiRqJvQ&os-sC&4OWt^(2@vG4|jui#Df@-D= zh3D%8Y3R6+jRBStSvH9pt&tCI`NK08J1*pC(?OM0h!bS-JK3I}`pDY-fDIaB_*W6KS+TO0Q*%kkeuN6uWITt=TsCGw6uBE710q; zRluI%j{?@jwhM|l5&TB!-TkQs!A=DXRE>u18t@;zndD0M$U@Igrt?UW2; z7%=dsHIVH_LCkGUU0fW&UMjDnvjcc0Mp(mK&;d~ZJ5EJ)#7@aTZvGDFXzFZg2Lq~s z5PR_LazNN)JD5K_uK*Hy{mXuHTkGGv|9V8KP#iQ$3!G*^>7UiE{|1G1A-qg(xH;Xa>&%f|BZkH zG=J^0pHzSAqv5*5ysQ{Puy^-_|IPrii zKS$mE10Zngf>Sgg@BjpRyJbrHeo zD8Ro0LI*W#+9?^xlOS^c>Z^^n^0I|FH^@^`ZR`{H=$ zjO0_$cnpBM7Zcm?H_RXIu-Lu~qweDSV|tEZBZh!e6hQy->}e;d#osZ1hQj{HhHkC0 zJ|F-HKmeTGgDe979ogBz24;@<|I7;TU!IXb@oWMsMECIETmQy`zPtM`|NP}PjzR_u zKMG1Z{%1kWeMfEf(10U#w!clmQ2)JC8zm(Fv!H4dUHQHCFLikID?hrd{0>kCQt?kP zdqn2ZG0}ytcQJ7t_B3s0ZvH3PYjkjQ`Q%;jV@?MK-+z3etBCGGo4f4`y^|AdCs!DH zThTQ;cL5dM{|tB_1y6K3bVa^hx_<9J(}5`2SDz1^0bT!Vm*JV;9~t&{IC{$DUAVV* z{|E=#yN{wNdTY@$6z{_KNA3&%w|vFu1n9XRcM0Ak>`UW!lQ`ah3D4r%}Z diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3499ded5c1..1af9e0930b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,5 +2,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421c..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ From a5aa0fdaefad39d6a0ce61023856ff17c6aa9802 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 29 Jan 2024 15:43:47 +1100 Subject: [PATCH 111/393] Add test for defer in list items --- ...eferExecutionSupportIntegrationTest.groovy | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index e7e1bfd2b8..0d98553d06 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -26,6 +26,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { def schemaSpec = ''' type Query { post : Post + posts: [Post] hello: String item(type: String!): Item } @@ -54,6 +55,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { coercionError: Int typeMismatchError: [String] nonNullableError: String! + wordCount: Int } type Comment { @@ -75,15 +77,19 @@ class DeferExecutionSupportIntegrationTest extends Specification { GraphQL graphQL = null private static DataFetcher resolve(Object value) { - return resolve(value, 0) + return resolve(value, 0, false) } private static DataFetcher resolve(Object value, Integer sleepMs) { + return resolve(value, sleepMs, false) + } + + private static DataFetcher resolve(Object value, Integer sleepMs, boolean allowMultipleCalls) { return new DataFetcher() { boolean executed = false; @Override Object get(DataFetchingEnvironment environment) throws Exception { - if(executed) { + if(executed && !allowMultipleCalls) { throw new IllegalStateException("This data fetcher can run only once") } executed = true; @@ -122,11 +128,17 @@ class DeferExecutionSupportIntegrationTest extends Specification { def runtimeWiring = RuntimeWiring.newRuntimeWiring() .type(newTypeWiring("Query") .dataFetcher("post", resolve([id: "1001"])) + .dataFetcher("posts", resolve([ + [id: "1001"], + [id: "1002"], + [id: "1003"] + ])) .dataFetcher("hello", resolve("world")) .dataFetcher("item", resolveItem()) ) .type(newTypeWiring("Post").dataFetcher("summary", resolve("A summary", 10))) .type(newTypeWiring("Post").dataFetcher("text", resolve("The full text", 100))) + .type(newTypeWiring("Post").dataFetcher("wordCount", resolve(45999, 10, true))) .type(newTypeWiring("Post").dataFetcher("latestComment", resolve([title: "Comment title"], 10))) .type(newTypeWiring("Post").dataFetcher("dataFetcherError", resolveWithException())) .type(newTypeWiring("Post").dataFetcher("coercionError", resolve("Not a number", 10))) @@ -1282,6 +1294,49 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "defer on list items"() { + def query = ''' + query { + posts { + id + ... @defer { + wordCount + } + } + } + ''' + + when: + def initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [posts: [[id: "1001"], [id: "1002"], [id: "1003"]]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + // Ordering is non-deterministic, so we assert on the things we know are going to be true. + + incrementalResults.size() == 3 + // only the last payload has "hasNext=true" + incrementalResults[0].hasNext == true + incrementalResults[1].hasNext == true + incrementalResults[2].hasNext == false + + // every payload has only 1 incremental item, and the data is the same for all of them + incrementalResults.every { it.incremental.size == 1 } + incrementalResults.every { it.incremental[0].data == [wordCount: 45999] } + + // path is different for every payload + incrementalResults.any { it.incremental[0].path == ["posts", 0] } + incrementalResults.any { it.incremental[0].path == ["posts", 1] } + incrementalResults.any { it.incremental[0].path == ["posts", 2] } + } + private ExecutionResult executeQuery(String query) { return this.executeQuery(query, true, [:]) } From 0542f1ee9a7db1125e3eb0a82f71d8f6de0fa123 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:38:17 +0000 Subject: [PATCH 112/393] Bump google-github-actions/auth from 2.0.1 to 2.1.0 Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.0.1 to 2.1.0. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v2.0.1...v2.1.0) --- updated-dependencies: - dependency-name: google-github-actions/auth dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/invoke_test_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index e23e1c5768..b955074934 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -50,7 +50,7 @@ jobs: - id: 'auth' name: 'Authenticate to Google Cloud' - uses: google-github-actions/auth@v2.0.1 + uses: google-github-actions/auth@v2.1.0 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} From f43849f8c8e65041e76b5d19148b210d30e8bb80 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 30 Jan 2024 08:25:03 +1100 Subject: [PATCH 113/393] Remove unnecessary test code --- .../rules/DeferredDirectiveAbstractRule.java | 38 ------- .../execution/defer/BasicSubscriber.groovy | 34 ------ .../defer/CapturingSubscriber.groovy | 1 + .../defer/DeferredErrorSupportTest.groovy | 77 ------------- .../defer/IncrementalContextDeferTest.groovy | 106 ++++++------------ 5 files changed, 34 insertions(+), 222 deletions(-) delete mode 100644 src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java delete mode 100644 src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy delete mode 100644 src/test/groovy/graphql/execution/defer/DeferredErrorSupportTest.groovy diff --git a/src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java b/src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java deleted file mode 100644 index 680f025c93..0000000000 --- a/src/main/java/graphql/validation/rules/DeferredDirectiveAbstractRule.java +++ /dev/null @@ -1,38 +0,0 @@ -package graphql.validation.rules; - -import graphql.Directives; -import graphql.Internal; -import graphql.language.Directive; -import graphql.language.Node; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLFieldDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.List; - -@Internal -public abstract class DeferredDirectiveAbstractRule extends AbstractRule { - - public DeferredDirectiveAbstractRule(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkDirective(Directive directive, List ancestors) { - if (!directive.getName().equals(Directives.DeferDirective.getName())) { - return; - } - - GraphQLCompositeType parentType = getValidationContext().getParentType(); - GraphQLFieldDefinition fieldDef = getValidationContext().getFieldDef(); - if (parentType == null || fieldDef == null) { - // some previous rule will have caught this - return; - } - onDeferredDirective(directive, ancestors, parentType, fieldDef); - } - - protected abstract void onDeferredDirective(Directive deferredDirective, List ancestors, GraphQLCompositeType parentType, GraphQLFieldDefinition fieldDef); -} diff --git a/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy b/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy deleted file mode 100644 index 564a41e2b6..0000000000 --- a/src/test/groovy/graphql/execution/defer/BasicSubscriber.groovy +++ /dev/null @@ -1,34 +0,0 @@ -package graphql.execution.defer - -import graphql.incremental.DelayedIncrementalExecutionResult -import org.reactivestreams.Subscriber -import org.reactivestreams.Subscription - -import java.util.concurrent.atomic.AtomicBoolean - -class BasicSubscriber implements Subscriber { - Subscription subscription - AtomicBoolean finished = new AtomicBoolean() - Throwable throwable - - @Override - void onSubscribe(Subscription s) { - assert s != null, "subscription must not be null" - this.subscription = s - s.request(1) - } - - @Override - void onNext(DelayedIncrementalExecutionResult executionResult) { - } - - @Override - void onError(Throwable t) { - finished.set(true) - } - - @Override - void onComplete() { - finished.set(true) - } -} diff --git a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy index 0334dc3d24..ec9f824090 100644 --- a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy +++ b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy @@ -42,6 +42,7 @@ class CapturingSubscriber implements Subscriber b.additionalDirective(Directives.DeferDirective) }) - def graphql = GraphQL.newGraphQL(schema).build() - - when: - def executionResult = graphql.execute(''' - { - stage1, - stage2 @defer - } - ''') - - then: - executionResult.errors.size() == 1 - executionResult.errors[0].getMessage().contains("bang-stage1") - - when: - def executionResultDeferred = null - def subscriber = new BasicSubscriber() { - @Override - void onNext(DelayedIncrementalExecutionResult executionResultStreamed) { - executionResultDeferred = executionResultStreamed - subscription.request(1) - } - } - Publisher deferredResultStream = executionResult.extensions[GraphQL.DEFERRED_RESULTS] as Publisher - deferredResultStream.subscribe(subscriber) - - await().untilTrue(subscriber.finished) - - then: - executionResultDeferred.errors.size() == 1 - executionResultDeferred.errors[0].getMessage().contains("bang-stage2") - - } -} diff --git a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy index 854382119c..802fb0e951 100644 --- a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy +++ b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy @@ -20,18 +20,9 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = [] - def subscriber = new BasicSubscriber() { - @Override - void onNext(DelayedIncrementalExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - } - incrementalContext.startDeferredCalls().subscribe(subscriber) - Awaitility.await().untilTrue(subscriber.finished) - then: + List results = startAndWaitCalls(incrementalContext) + then: assertResultsSizeAndHasNextRule(3, results) results[0].incremental[0].data["c"] == "C" results[1].incremental[0].data["b"] == "B" @@ -46,19 +37,9 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThreadCallWithinCall(incrementalContext, "C", "C_Child", 100, "/c")) when: - List results = [] - BasicSubscriber subscriber = new BasicSubscriber() { - @Override - void onNext(DelayedIncrementalExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - } - incrementalContext.startDeferredCalls().subscribe(subscriber) + List results = startAndWaitCalls(incrementalContext) - Awaitility.await().untilTrue(subscriber.finished) then: - assertResultsSizeAndHasNextRule(6, results) results[0].incremental[0].data["c"] == "C" results[1].incremental[0].data["c_child"] == "C_Child" @@ -76,21 +57,7 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) when: - List results = [] - Throwable thrown = null - def subscriber = new BasicSubscriber() { - @Override - void onNext(DelayedIncrementalExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - - @Override - void onError(Throwable t) { - thrown = t - finished.set(true) - } - + def subscriber = new CapturingSubscriber() { @Override void onComplete() { assert false, "This should not be called!" @@ -99,8 +66,11 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) - then: + def results = subscriber.executionResults + def thrown = subscriber.throwable + + then: thrown.message == "java.lang.RuntimeException: Bang" results[0].incremental[0].data["c"] == "C" } @@ -113,11 +83,10 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = [] - def subscriber = new BasicSubscriber() { + def subscriber = new CapturingSubscriber() { @Override void onNext(DelayedIncrementalExecutionResult executionResult) { - results.add(executionResult) + this.executionResults.add(executionResult) subscription.cancel() finished.set(true) } @@ -125,15 +94,17 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.finished) - then: + def results = subscriber.executionResults + then: results.size() == 1 results[0].incremental[0].data["c"] == "C" - // Cancelling the subscription will result in an invalid state. The last result item will have "hasNext=true". + // Cancelling the subscription will result in an invalid state. + // The last result item will have "hasNext=true" (but there will be no next) results[0].hasNext } - def "you cant subscribe twice"() { + def "you can't subscribe twice"() { given: def incrementalContext = new IncrementalContext() incrementalContext.enqueue(offThread("A", 100, "/field/path")) @@ -141,16 +112,14 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - Throwable expectedThrowable - incrementalContext.startDeferredCalls().subscribe(new BasicSubscriber()) - incrementalContext.startDeferredCalls().subscribe(new BasicSubscriber() { - @Override - void onError(Throwable t) { - expectedThrowable = t - } - }) + def subscriber1 = new CapturingSubscriber() + def subscriber2 = new CapturingSubscriber() + incrementalContext.startDeferredCalls().subscribe(subscriber1) + incrementalContext.startDeferredCalls().subscribe(subscriber2) + then: - expectedThrowable != null + subscriber2.throwable != null + subscriber2.throwable.message == "This publisher only supports one subscriber" } def "indicates if there are any defers present"() { @@ -199,17 +168,8 @@ class IncrementalContextDeferTest extends Specification { def incrementalContext = new IncrementalContext() incrementalContext.enqueue(deferredCall) - List results = [] - BasicSubscriber subscriber = new BasicSubscriber() { - @Override - void onNext(DelayedIncrementalExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - } - incrementalContext.startDeferredCalls().subscribe(subscriber) + def results = startAndWaitCalls(incrementalContext) - Awaitility.await().untilTrue(subscriber.finished) then: assertResultsSizeAndHasNextRule(1, results) results[0].incremental[0].data["call1"] == "Call 1" @@ -224,16 +184,7 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = [] - def subscriber = new BasicSubscriber() { - @Override - void onNext(DelayedIncrementalExecutionResult executionResult) { - results.add(executionResult) - subscription.request(1) - } - } - incrementalContext.startDeferredCalls().subscribe(subscriber) - Awaitility.await().untilTrue(subscriber.finished) + List results = startAndWaitCalls(incrementalContext) then: "hasNext placement should be deterministic - only the last event published should have 'hasNext=true'" assertResultsSizeAndHasNextRule(3, results) @@ -286,4 +237,13 @@ class IncrementalContextDeferTest extends Specification { || (!hasNext && isLastResult) } } + + private static List startAndWaitCalls(IncrementalContext incrementalContext) { + def subscriber = new CapturingSubscriber() + + incrementalContext.startDeferredCalls().subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.finished) + return subscriber.executionResults + } } From 98554dc0e56b55bf43dee5055325e441a2e873d2 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 30 Jan 2024 08:36:08 +1100 Subject: [PATCH 114/393] Javadoc --- src/main/java/graphql/ExecutionInput.java | 14 ++++++++------ src/main/java/graphql/execution/Execution.java | 8 +++++--- src/main/java/graphql/execution/MergedField.java | 6 +++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index 46bec9ec3a..6a82ad61f6 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -142,15 +142,15 @@ public Map getExtensions() { } /** - * TODO: Javadoc - * @return + * @return whether the execution has support for incremental delivery of data, via the @defer and @stream directives. + * + * This is currently an experimental feature, and only @defer is supported. */ @ExperimentalApi public boolean isIncrementalSupport() { return incrementalSupport; } - /** * 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. @@ -394,9 +394,11 @@ public Builder dataLoaderRegistry(DataLoaderRegistry dataLoaderRegistry) { } /** - * TODO: Javadoc - * @param incrementalSupport - * @return + * @param incrementalSupport whether the execution has support for incremental delivery of data, via the @defer and @stream directives. + *

+ * This is currently an experimental feature, and only @defer is supported. + * + * @return this builder */ @ExperimentalApi public Builder incrementalSupport(boolean incrementalSupport) { diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 161babf86a..e6ebf27709 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -196,12 +196,14 @@ private CompletableFuture deferSupport(ExecutionContext executi return result.thenApply(er -> { IncrementalContext deferSupport = executionContext.getIncrementalContext(); if (deferSupport.isDeferDetected()) { - // we start the rest of the query now to maximize throughput. We have the initial important results - // and now we can start the rest of the calls as early as possible (even before some one subscribes) + // we start the rest of the query now to maximize throughput. We have the initial important results, + // and now we can start the rest of the calls as early as possible (even before someone subscribes) Publisher publisher = deferSupport.startDeferredCalls(); return IncrementalExecutionResultImpl.fromExecutionResult(er) - // TODO: would `hasNext` ever be false? + // "hasNext" can, in theory, be "false" when all the incremental items are delivered in the + // first response payload. However, the current implementation will never result in this. + // The behaviour might change if we decide to make optimizations in the future. .hasNext(true) .incrementalItemPublisher(publisher) .build(); diff --git a/src/main/java/graphql/execution/MergedField.java b/src/main/java/graphql/execution/MergedField.java index e160bc8622..1ebc53d504 100644 --- a/src/main/java/graphql/execution/MergedField.java +++ b/src/main/java/graphql/execution/MergedField.java @@ -126,9 +126,9 @@ public List getFields() { } /** - * TODO: Javadoc - * TODO: should be getIncrementalExecutions? - * @return + * Get a list of all {@link DeferExecution}s that this field is part of + * + * @return all defer executions. */ @ExperimentalApi public ImmutableList getDeferExecutions() { From ccb7e705cbaff10ccc8cafe2c989c5da60738755 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 30 Jan 2024 08:44:13 +1100 Subject: [PATCH 115/393] Refactor IncrementalUtils to remove duplicated code --- src/main/java/graphql/execution/FieldCollector.java | 6 ++++-- .../execution/incremental/IncrementalUtils.java | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index fec5619b1a..6e5e411812 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -108,7 +108,8 @@ private void collectFragmentSpread(FieldCollectorParameters parameters, Set T createDeferExecution( Map variables, - List directives + List directives, + Function builderFunction ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -38,12 +40,12 @@ public static DeferExecution createDeferExecution( Object label = argumentValues.get("label"); if (label == null) { - return new DeferExecution(null); + return builderFunction.apply(null); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label); + return builderFunction.apply((String) label); } return null; From f515d8909bc3fdd5ab7146a7c7a63f58be7e5a30 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 30 Jan 2024 11:18:33 +1100 Subject: [PATCH 116/393] Updated field level approach --- .../FieldLevelTrackingApproach.java | 97 +++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index a98fac3ae0..93029ee269 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -8,13 +8,14 @@ import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import graphql.util.LockKit; import org.dataloader.DataLoaderRegistry; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; @@ -25,11 +26,8 @@ @Internal public class FieldLevelTrackingApproach { private final Supplier dataLoaderRegistrySupplier; - private static class CallStack implements InstrumentationState { - private final LockKit.ReentrantLock lock = new LockKit.ReentrantLock(); - private final LevelMap expectedFetchCountPerLevel = new LevelMap(); private final LevelMap fetchCountPerLevel = new LevelMap(); private final LevelMap expectedStrategyCallsPerLevel = new LevelMap(); @@ -120,14 +118,8 @@ public InstrumentationState createState() { ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { CallStack callStack = (CallStack) rawState; - ResultPath path = parameters.getExecutionStrategyParameters().getPath(); - int parentLevel = path.getLevel(); - int curLevel = parentLevel + 1; - int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); - callStack.lock.runLocked(() -> { - callStack.increaseExpectedFetchCount(curLevel, fieldCount); - callStack.increaseHappenedStrategyCalls(curLevel); - }); + int curLevel = getCurrentLevel(parameters); + increaseCallCounts(callStack, curLevel, parameters); return new ExecutionStrategyInstrumentationContext() { @Override @@ -142,25 +134,74 @@ public void onCompleted(ExecutionResult result, Throwable t) { @Override public void onFieldValuesInfo(List fieldValueInfoList) { - boolean dispatchNeeded = callStack.lock.callLocked(() -> - handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel) - ); - if (dispatchNeeded) { - dispatch(); + onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); + } + + @Override + public void onFieldValuesException() { + synchronized (callStack) { + callStack.increaseHappenedOnFieldValueCalls(curLevel); } } + }; + } + + ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { + CallStack callStack = (CallStack) rawState; + int curLevel = getCurrentLevel(parameters); + increaseCallCounts(callStack, curLevel, parameters); + + return new ExecuteObjectInstrumentationContext() { + + @Override + public void onDispatched(CompletableFuture> result) { + } + + @Override + public void onCompleted(Map result, Throwable t) { + } + + @Override + public void onFieldValuesInfo(List fieldValueInfoList) { + onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); + } @Override public void onFieldValuesException() { - callStack.lock.runLocked(() -> - callStack.increaseHappenedOnFieldValueCalls(curLevel) - ); + synchronized (callStack) { + callStack.increaseHappenedOnFieldValueCalls(curLevel); + } } }; } + private int getCurrentLevel(InstrumentationExecutionStrategyParameters parameters) { + ResultPath path = parameters.getExecutionStrategyParameters().getPath(); + return path.getLevel() + 1; + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private static void increaseCallCounts(CallStack callStack, int curLevel, InstrumentationExecutionStrategyParameters parameters) { + int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); + synchronized (callStack) { + callStack.increaseExpectedFetchCount(curLevel, fieldCount); + callStack.increaseHappenedStrategyCalls(curLevel); + } + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private void onFieldValuesInfoDispatchIfNeeded(CallStack callStack, List fieldValueInfoList, int curLevel) { + boolean dispatchNeeded; + synchronized (callStack) { + dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); + } + if (dispatchNeeded) { + dispatch(); + } + } + // - // thread safety : called with synchronised(callStack) + // thread safety: called with synchronised(callStack) // private boolean handleOnFieldValuesInfo(List fieldValueInfos, CallStack callStack, int curLevel) { callStack.increaseHappenedOnFieldValueCalls(curLevel); @@ -186,17 +227,19 @@ public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchP CallStack callStack = (CallStack) rawState; ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); int level = path.getLevel(); - return new InstrumentationContext<>() { + return new InstrumentationContext() { @Override public void onDispatched(CompletableFuture result) { - boolean dispatchNeeded = callStack.lock.callLocked(() -> { + boolean dispatchNeeded; + synchronized (callStack) { callStack.increaseFetchCount(level); - return dispatchIfNeeded(callStack, level); - }); + dispatchNeeded = dispatchIfNeeded(callStack, level); + } if (dispatchNeeded) { dispatch(); } + } @Override @@ -217,7 +260,7 @@ private boolean dispatchIfNeeded(CallStack callStack, int level) { } // - // thread safety : called with synchronised(callStack) + // thread safety: called with synchronised(callStack) // private boolean levelReady(CallStack callStack, int level) { if (level == 1) { @@ -239,4 +282,4 @@ void dispatch() { private DataLoaderRegistry getDataLoaderRegistry() { return dataLoaderRegistrySupplier.get(); } -} +} \ No newline at end of file From 143d089c1a07a7fd6df5e50d50ca140156d01a23 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 30 Jan 2024 11:46:21 +1100 Subject: [PATCH 117/393] Fixed up @DeprecatedAT --- src/main/java/graphql/execution/ExecutionStrategy.java | 1 + src/main/java/graphql/execution/FieldValueInfo.java | 4 +--- .../execution/instrumentation/Instrumentation.java | 9 +++------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5f5e0b917b..ffdf9b1dfd 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -43,6 +43,7 @@ import graphql.schema.GraphQLType; import graphql.schema.LightDataFetcher; import graphql.util.FpKit; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index ff0f7817c1..571bbaed1a 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -1,6 +1,5 @@ package graphql.execution; -import graphql.DeprecatedAt; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.PublicApi; @@ -38,8 +37,7 @@ public CompleteValueType getCompleteValueType() { return completeValueType; } - @Deprecated - @DeprecatedAt("2023-09-11") + @Deprecated(since="2023-09-11" ) public CompletableFuture getFieldValue() { return fieldValue.thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()); } diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 6323125c89..54088fb8aa 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -290,8 +290,7 @@ default InstrumentationContext beginField(InstrumentationFieldP * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ - @Deprecated - @DeprecatedAt("2023-09-11") + @Deprecated(since="2023-09-11" ) @Nullable default InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { return beginField(parameters.withNewState(state)); @@ -363,8 +362,7 @@ default InstrumentationContext beginFieldComplete(Instrumentati * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ - @Deprecated - @DeprecatedAt("2023-09-11") + @Deprecated(since = "2023-09-11") @Nullable default InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return beginFieldComplete(parameters.withNewState(state)); @@ -407,8 +405,7 @@ default InstrumentationContext beginFieldListComplete(Instrumen * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ - @Deprecated - @DeprecatedAt("2023-09-11") + @Deprecated(since = "2023-09-11") @Nullable default InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return beginFieldListComplete(parameters.withNewState(state)); From ccfaaf4628039bf67ea423706ffe9d35ce102af7 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 30 Jan 2024 18:19:00 +1100 Subject: [PATCH 118/393] Fix up abort exception so its now an error --- .../graphql/execution/ExecutionStrategy.java | 16 +++++++++++----- .../ExecutionStrategyErrorsTest.groovy | 17 +++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index ffdf9b1dfd..edd01a981e 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -203,7 +203,7 @@ protected CompletableFuture> executeObject(ExecutionContext resolveObjectCtx.onDispatched(overallResult); resolvedFieldFutures.await().whenComplete((completeValueInfos, throwable) -> { - BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldNames, overallResult); + BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldNames, overallResult,executionContext); if (throwable != null) { handleResultsConsumer.accept(null, throwable); return; @@ -228,10 +228,10 @@ protected CompletableFuture> executeObject(ExecutionContext return overallResult; } - private BiConsumer, Throwable> buildFieldValueMap(List fieldNames, CompletableFuture> overallResult) { + private BiConsumer, Throwable> buildFieldValueMap(List fieldNames, CompletableFuture> overallResult, ExecutionContext executionContext) { return (List results, Throwable exception) -> { if (exception != null) { - handleValueException(overallResult, exception); + handleValueException(overallResult, exception, executionContext); return; } Map resolvedValuesByField = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size()); @@ -681,7 +681,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, resultsFuture.whenComplete((results, exception) -> { if (exception != null) { - handleValueException(overallResult, exception); + handleValueException(overallResult, exception, executionContext); return; } List completedResults = new ArrayList<>(results.size()); @@ -695,7 +695,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, .build(); } - protected void handleValueException(CompletableFuture overallResult, Throwable e) { + protected void handleValueException(CompletableFuture overallResult, Throwable e, ExecutionContext executionContext) { Throwable underlyingException = e; if (e instanceof CompletionException) { underlyingException = e.getCause(); @@ -705,6 +705,12 @@ protected void handleValueException(CompletableFuture overallResult, Thro if (!overallResult.isDone()) { overallResult.complete(null); } + } else if (underlyingException instanceof AbortExecutionException) { + AbortExecutionException abortException = (AbortExecutionException) underlyingException; + executionContext.addError(abortException); + if (!overallResult.isDone()) { + overallResult.complete(null); + } } else { overallResult.completeExceptionally(e); } diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy index 79a6ae965a..55b6afb6cb 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyErrorsTest.groovy @@ -2,7 +2,6 @@ package graphql.execution import graphql.ExceptionWhileDataFetching import graphql.ExecutionInput -import graphql.ExecutionResult import graphql.GraphQL import graphql.SerializationError import graphql.TestUtil @@ -108,7 +107,7 @@ class ExecutionStrategyErrorsTest extends Specification { def er = graphQL.execute(ei) then: - er.errors.size() == 6 + er.errors.size() == 7 er.errors[0] instanceof TypeMismatchError er.errors[0].path == ["notAList"] @@ -116,17 +115,19 @@ class ExecutionStrategyErrorsTest extends Specification { er.errors[1].path == ["notAFloat"] er.errors[2] instanceof ExceptionWhileDataFetching - er.errors[2].path ==["notAnProperObject", "diceyListCall", 0, "bang"] - ((ExceptionWhileDataFetching)er.errors[2]).exception.message == "dicey call" + er.errors[2].path == ["notAnProperObject", "diceyListCall", 0, "bang"] + ((ExceptionWhileDataFetching) er.errors[2]).exception.message == "dicey call" er.errors[3] instanceof ExceptionWhileDataFetching - er.errors[3].path ==["notAnProperObject", "diceyListCall", 0, "abort"] - ((ExceptionWhileDataFetching)er.errors[3]).exception.message == "abort abort" + er.errors[3].path == ["notAnProperObject", "diceyListCall", 0, "abort"] + ((ExceptionWhileDataFetching) er.errors[3]).exception.message == "abort abort" er.errors[4] instanceof NonNullableFieldWasNullError - er.errors[4].path ==["notAnProperObject", "diceyListCall", 0, "nonNull"] + er.errors[4].path == ["notAnProperObject", "diceyListCall", 0, "nonNull"] er.errors[5] instanceof NonNullableFieldWasNullError - er.errors[5].path ==["notAnProperObject", "diceyListCall", 1] // the entry itself was null in a non null list entry + er.errors[5].path == ["notAnProperObject", "diceyListCall", 1] // the entry itself was null in a non null list entry + + er.errors[6] instanceof AbortExecutionException } } From e0540d9270a3ecd7eb0d121d6bb905cf30675c3b Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 1 Feb 2024 14:11:08 +1100 Subject: [PATCH 119/393] WIP --- .../execution/AsyncExecutionStrategy.java | 40 ++++++++++-- .../graphql/execution/defer/DeferredCall.java | 20 ++++-- .../ChainedInstrumentation.java | 65 ++++++++++--------- .../DeferredFieldInstrumentationContext.java | 5 +- .../instrumentation/Instrumentation.java | 22 +++++-- .../DataLoaderDispatcherInstrumentation.java | 5 +- .../FieldLevelTrackingApproach.java | 44 ++++++++----- ...nstrumentationDeferredFieldParameters.java | 17 ++--- .../DelayedIncrementalExecutionResult.java | 3 +- .../execution/defer/DeferredCallTest.groovy | 62 +++++++++++++----- .../DataLoaderPerformanceData.groovy | 10 +-- ...manceWithChainedInstrumentationTest.groovy | 7 +- .../SubscriptionUniqueRootFieldTest.groovy | 13 +++- 13 files changed, 207 insertions(+), 106 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 850a799361..58bcb4e5ac 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -8,8 +8,10 @@ import graphql.execution.defer.DeferredCall; import graphql.execution.defer.DeferredCallContext; import graphql.execution.incremental.DeferExecution; +import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.util.FpKit; @@ -192,7 +194,7 @@ public Set createCalls() { List mergedFields = deferExecutionToFields.get(deferExecution); - List>> collect = mergedFields.stream() + List>> calls = mergedFields.stream() .map(currentField -> { Map fields = new LinkedHashMap<>(); fields.put(currentField.getName(), currentField); @@ -208,14 +210,33 @@ public Set createCalls() { } ); + + Instrumentation instrumentation = executionContext.getInstrumentation(); + DeferredFieldInstrumentationContext fieldCtx = instrumentation.beginDeferredField( + new InstrumentationDeferredFieldParameters(executionContext, parameters), executionContext.getInstrumentationState() + ); + + return dfCache.computeIfAbsent( currentField.getName(), // The same field can be associated with multiple defer executions, so // we memoize the field resolution to avoid multiple calls to the same data fetcher - key -> FpKit.interThreadMemoize(() -> resolveFieldWithInfoFn - .apply(executionContext, callParameters) - .thenCompose(FieldValueInfo::getFieldValue) - .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult))) + key -> FpKit.interThreadMemoize(() -> { + CompletableFuture fieldValueResult = resolveFieldWithInfoFn.apply(executionContext, callParameters); + + fieldCtx.onDispatched(null); + + return fieldValueResult + .thenApply(fieldValueInfo -> { + System.out.println("then apply: " + fieldValueInfo.getFieldValue().isDone()); + fieldCtx.onFieldValueInfo(fieldValueInfo); + return fieldValueInfo; + }) + .thenCompose(FieldValueInfo::getFieldValue) + .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult)); + } +// .whenComplete(fieldCtx::onCompleted) + ) ); }) @@ -225,12 +246,19 @@ public Set createCalls() { return new DeferredCall( deferExecution.getLabel(), this.parameters.getPath(), - collect, + calls, errorSupport ); }) .collect(Collectors.toSet()); } + +// private ExecutionStepInfo createSubscribedFieldStepInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { +// Field field = parameters.getField().getSingleField(); +// GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); +// GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field); +// return createExecutionStepInfo(executionContext, parameters, fieldDef, parentType); +// } } class NoOp implements DeferExecutionSupport { diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index 2f2a6ecfd3..88b1a31ed2 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -8,6 +8,7 @@ import graphql.execution.NonNullableFieldWasNullError; import graphql.execution.NonNullableFieldWasNullException; import graphql.execution.ResultPath; +import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.incremental.DeferPayload; import java.util.Collections; @@ -39,9 +40,14 @@ @Internal public class DeferredCall implements IncrementalCall { private final String label; + + public ResultPath getPath() { + return path; + } + private final ResultPath path; private final List>> calls; - private final DeferredCallContext errorSupport; + private final DeferredCallContext deferredCallContext; public DeferredCall( String label, @@ -52,14 +58,18 @@ public DeferredCall( this.label = label; this.path = path; this.calls = calls; - this.errorSupport = deferredErrorSupport; + this.deferredCallContext = deferredErrorSupport; } @Override public CompletableFuture invoke() { + System.out.println("DeferredCall.invoke(): " + this.getPath()); Async.CombinedBuilder futures = Async.ofExpectedSize(calls.size()); - calls.forEach(call -> futures.add(call.get())); + calls.forEach(call -> { + CompletableFuture cf = call.get(); + futures.add(cf); + }); return futures.await() .thenApply(this::transformToDeferredPayload) @@ -73,6 +83,7 @@ public CompletableFuture invoke() { * and build a special {@link DeferPayload} that captures the details of the error. */ private DeferPayload handleNonNullableFieldError(DeferPayload result, Throwable throwable) { + System.out.println("DeferredCall.handleNonNullableFieldError(): " + this.getPath()); if (throwable != null) { Throwable cause = throwable.getCause(); if (cause instanceof NonNullableFieldWasNullException) { @@ -92,7 +103,8 @@ private DeferPayload handleNonNullableFieldError(DeferPayload result, Throwable } private DeferPayload transformToDeferredPayload(List fieldWithExecutionResults) { - List errorsEncountered = errorSupport.getErrors(); + System.out.println("DeferredCall.transformToDeferredPayload(): " + this.getPath()); + List errorsEncountered = deferredCallContext.getErrors(); Map dataMap = new HashMap<>(); diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 1a3089584b..ca475bca1a 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -9,6 +9,7 @@ import graphql.execution.Async; import graphql.execution.ExecutionContext; import graphql.execution.FieldValueInfo; +import graphql.execution.defer.DeferredCall; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; @@ -26,10 +27,12 @@ import org.jetbrains.annotations.Nullable; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; import static graphql.collect.ImmutableKit.mapAndDropNulls; @@ -177,16 +180,14 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument } @Override - public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { -// return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() -// .map(instrumentation -> { -// InstrumentationState specificState = getSpecificState(instrumentation, parameters.getInstrumentationState()); -// return instrumentation.beginDeferredField(parameters, specificState); -// }) -// .collect(Collectors.toList())); + public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState instrumentationState) { + return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() + .map(instrumentation -> { + InstrumentationState specificState = getSpecificState(instrumentation, instrumentationState); + return instrumentation.beginDeferredField(parameters, specificState); + }) + .collect(Collectors.toList())); - // TODO: Fix this - throw new UnsupportedOperationException("TODO: fix this"); } @Override @@ -448,28 +449,28 @@ public void onFieldValuesException() { } } -// private static class ChainedDeferredExecutionStrategyInstrumentationContext implements DeferredFieldInstrumentationContext { -// -// private final List contexts; -// -// ChainedDeferredExecutionStrategyInstrumentationContext(List> contexts) { -// this.contexts = Collections.unmodifiableList(contexts); -// } -// -// @Override -// public void onDispatched(CompletableFuture result) { -// contexts.forEach(context -> context.onDispatched(result)); -// } -// -// @Override -// public void onCompleted(ExecutionResult result, Throwable t) { -// contexts.forEach(context -> context.onCompleted(result, t)); -// } -// -// @Override -// public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { -// contexts.forEach(context -> context.onFieldValueInfo(fieldValueInfo)); -// } -// } + private static class ChainedDeferredExecutionStrategyInstrumentationContext implements DeferredFieldInstrumentationContext { + + private final List contexts; + + ChainedDeferredExecutionStrategyInstrumentationContext(List contexts) { + this.contexts = Collections.unmodifiableList(contexts); + } + + @Override + public void onDispatched(CompletableFuture result) { + contexts.forEach(context -> context.onDispatched(result)); + } + + @Override + public void onCompleted(DeferredCall.FieldWithExecutionResult result, Throwable t) { + contexts.forEach(context -> context.onCompleted(result, t)); + } + + @Override + public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + contexts.forEach(context -> context.onFieldValueInfo(fieldValueInfo)); + } + } } diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java index 39de62be19..d700f3c58a 100644 --- a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java @@ -1,11 +1,12 @@ package graphql.execution.instrumentation; -import graphql.ExecutionResult; import graphql.execution.FieldValueInfo; +import graphql.execution.defer.DeferredCall; -public interface DeferredFieldInstrumentationContext extends InstrumentationContext { +public interface DeferredFieldInstrumentationContext extends InstrumentationContext { default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + System.out.println("DeferredFieldInstrumentationContext.onFieldValueInfo() [default]: " + fieldValueInfo); } diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index aed0e707b7..f6c70fd0e8 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -4,6 +4,7 @@ import graphql.ExecutionResult; import graphql.PublicSpi; import graphql.execution.ExecutionContext; +import graphql.execution.defer.DeferredCall; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; @@ -224,14 +225,21 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen } /** - * This is called just before a deferred field is resolved into a value. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends + * TODO Javadoc + * @return */ - default InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { - return noOp(); + default DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState instrumentationState) { + return new DeferredFieldInstrumentationContext() { + @Override + public void onDispatched(CompletableFuture result) { + + } + + @Override + public void onCompleted(DeferredCall.FieldWithExecutionResult result, Throwable t) { + + } + }; } /** diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 4ce01319ed..b1aa63247d 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -7,6 +7,7 @@ import graphql.execution.AsyncExecutionStrategy; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategy; +import graphql.execution.defer.DeferredCall; import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; @@ -145,11 +146,11 @@ public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDef if (state.hasNoDataLoaders()) { return new DeferredFieldInstrumentationContext() { @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched(CompletableFuture result) { } @Override - public void onCompleted(ExecutionResult result, Throwable t) { + public void onCompleted(DeferredCall.FieldWithExecutionResult result, Throwable t) { } }; diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 3dc207b7a4..4c8865a967 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -5,7 +5,7 @@ import graphql.Internal; import graphql.execution.FieldValueInfo; import graphql.execution.ResultPath; -import graphql.execution.MergedField; +import graphql.execution.defer.DeferredCall; import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; @@ -18,7 +18,6 @@ import org.slf4j.Logger; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -190,8 +189,7 @@ private int getCountForList(List fieldValueInfos) { } DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { - // TODO: Is this cast safe? - CallStack callStack = (CallStack) state; + CallStack callStack = new CallStack(); int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); synchronized (callStack) { callStack.clearAndMarkCurrentLevelAsReady(level); @@ -199,24 +197,34 @@ DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFi return new DeferredFieldInstrumentationContext() { @Override - public void onDispatched(CompletableFuture result) { - + public void onDispatched(CompletableFuture result) { + boolean dispatchNeeded = callStack.lock.callLocked(() -> { + callStack.increaseFetchCount(level); + return dispatchIfNeeded(callStack, level); + }); + if (dispatchNeeded) { + // TODO: I think "dispatch" is being called before it should + System.out.println("InstrumentationContext.onDispatched(): dispatching"); + dispatch(); + } } @Override - public void onCompleted(ExecutionResult result, Throwable t) { + public void onCompleted(DeferredCall.FieldWithExecutionResult result, Throwable t) { + System.out.println("DeferredFieldInstrumentationContext.onCompleted(): " + parameters.getExecutionStrategyParameters().getPath()); } - @Override - public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - boolean dispatchNeeded; - synchronized (callStack) { - dispatchNeeded = handleOnFieldValuesInfo(Collections.singletonList(fieldValueInfo), callStack, level); - } - if (dispatchNeeded) { - dispatch(); - } - } +// @Override +// public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { +// System.out.println("DeferredFieldInstrumentationContext.onFieldValueInfo(): " + parameters.getExecutionStrategyParameters().getPath()); +// boolean dispatchNeeded; +// synchronized (callStack) { +// dispatchNeeded = handleOnFieldValuesInfo(Collections.singletonList(fieldValueInfo), callStack, level); +// } +// if (dispatchNeeded) { +// dispatch(); +// } +// } }; } @@ -233,6 +241,8 @@ public void onDispatched(CompletableFuture result) { return dispatchIfNeeded(callStack, level); }); if (dispatchNeeded) { + // TODO: I think "dispatch" is being called before it should + System.out.println("InstrumentationContext.onDispatched(): dispatching"); dispatch(); } } diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java index 64714049b2..df67d63313 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java @@ -10,20 +10,21 @@ /** * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods */ -public class InstrumentationDeferredFieldParameters extends InstrumentationFieldParameters { - +public class InstrumentationDeferredFieldParameters { + private final ExecutionContext executionContext; private final ExecutionStrategyParameters executionStrategyParameters; - public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, ExecutionStrategyParameters executionStrategyParameters) { - this(executionContext, executionStepInfo, executionContext.getInstrumentationState(), executionStrategyParameters); - } - - InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, InstrumentationState instrumentationState, ExecutionStrategyParameters executionStrategyParameters) { - super(executionContext, executionStepInfo, instrumentationState); + public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters) { + this.executionContext = executionContext; this.executionStrategyParameters = executionStrategyParameters; } + public ExecutionStrategyParameters getExecutionStrategyParameters() { return executionStrategyParameters; } + + public ExecutionContext getExecutionContext() { + return executionContext; + } } diff --git a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java index abce52f359..3e618a16ce 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java @@ -37,8 +37,7 @@ public interface DelayedIncrementalExecutionResult { Map getExtensions(); /** - * TODO: Javadoc - * @return + * @return a map of the result that strictly follows the spec */ Map toSpecification(); } diff --git a/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy b/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy index 5db3a149d9..a05b52e569 100644 --- a/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy +++ b/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy @@ -1,9 +1,9 @@ package graphql.execution.defer - import graphql.ExecutionResultImpl -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorType +import graphql.GraphQLError +import graphql.execution.NonNullableFieldWasNullException +import graphql.execution.ResultPath import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -50,41 +50,69 @@ class DeferredCallTest extends Specification { ] } - def "test error capture happens via CF"() { + def "can handle non-nullable field error"() { given: - def errorSupport = new DeferredCallContext() - errorSupport.onError(new ValidationError(ValidationErrorType.MissingFieldArgument)) - errorSupport.onError(new ValidationError(ValidationErrorType.FieldsConflict)) + def deferredCallContext = new DeferredCallContext() + def mockedException = Mock(NonNullableFieldWasNullException) { + getMessage() >> "Field value can't be null" + getPath() >> ResultPath.parse("/path") + } - DeferredCall call = new DeferredCall(parse("/path"), { - completedFuture(new ExecutionResultImpl("some data", [new ValidationError(ValidationErrorType.FieldUndefined)])) - }, errorSupport) + DeferredCall call = new DeferredCall("my-label", parse("/path"), [ + createFieldCallThatThrowsException(mockedException), + createResolvedFieldCall("field1", "some data") + ], deferredCallContext) when: def future = call.invoke() - def er = future.join() + def deferPayload = future.join() then: - er.errors.size() == 3 - er.errors[0].message.contains("Validation error of type FieldUndefined") - er.errors[1].message.contains("Validation error of type MissingFieldArgument") - er.errors[2].message.contains("Validation error of type FieldsConflict") - er.path == ["path"] + deferPayload.toSpecification() == [ + path : ["path"], + label : "my-label", + errors: [ + [ + message : "Field value can't be null", + path : ["path"], + extensions: [classification: "NullValueInNonNullableField"] + ] + ], + ] } private static Supplier> createResolvedFieldCall( String fieldName, Object data + ) { + return createResolvedFieldCall(fieldName, data, Collections.emptyList()) + } + + private static Supplier> createResolvedFieldCall( + String fieldName, + Object data, + List errors ) { return new Supplier>() { @Override CompletableFuture get() { return completedFuture( new DeferredCall.FieldWithExecutionResult(fieldName, - new ExecutionResultImpl(data, Collections.emptyList()) + new ExecutionResultImpl(data, errors) ) ) } } } + + private static Supplier> createFieldCallThatThrowsException( + Throwable exception + ) { + return new Supplier>() { + @Override + CompletableFuture get() { + return CompletableFuture.failedFuture(exception) + } + } + } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy index a0fec6c9da..5ec34abc24 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy @@ -196,12 +196,14 @@ class DataLoaderPerformanceData { query { shops { id name - departments @defer { - id name - products { + ... @defer { + departments { id name + products { + id name + } } - } + } } } """ diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index dae4965238..897dc32edb 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -110,7 +110,11 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification when: - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(deferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(deferredQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(true) + .build() def result = graphQL.execute(executionInput) Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher @@ -123,7 +127,6 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification then: result.data == expectedInitialDeferredData - subscriber.executionResultData == expectedListOfDeferredData // diff --git a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy b/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy index 1e2d72756f..f37e683c05 100644 --- a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy +++ b/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy @@ -12,8 +12,15 @@ class SubscriptionUniqueRootFieldTest extends Specification { given: def subscriptionOneRoot = ''' subscription doggo { - dog { - name + ... { + dog { + name + } + ... { + dog2: dog { + name + } + } } } ''' @@ -22,7 +29,7 @@ class SubscriptionUniqueRootFieldTest extends Specification { def validationErrors = validate(subscriptionOneRoot) then: - validationErrors.empty + !validationErrors.empty } def "5.2.3.1 subscription with only one root field with fragment passes validation"() { From b3afb5f7ee3b8339795154ac26b49cd2e2b066c0 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 1 Feb 2024 14:11:50 +1100 Subject: [PATCH 120/393] Fix oneOf bug when variables is empty --- .../ValuesResolverOneOfValidation.java | 39 ++++++++++++------- .../execution/ValuesResolverTest.groovy | 3 ++ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index 6456ffaa01..c0f98f5523 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -75,23 +75,36 @@ static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value argumentValue, Map objectMap, Locale locale) { - int mapSize; - + final String fieldName; if (argumentValue instanceof ObjectValue) { - mapSize = ((ObjectValue) argumentValue).getObjectFields().size(); + List objectFields = ((ObjectValue) argumentValue).getObjectFields(); + if (objectFields.size() != 1) { + throwNotOneFieldError(oneOfInputType, locale); + } + + fieldName = objectFields.iterator().next().getName(); } else { - mapSize = objectMap.size(); - } - if (mapSize != 1) { - String msg = I18n.i18n(I18n.BundleType.Execution, locale) - .msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName()); - throw new OneOfTooManyKeysException(msg); + if (objectMap.size() != 1) { + throwNotOneFieldError(oneOfInputType, locale); + } + + fieldName = objectMap.keySet().iterator().next(); } - String fieldName = objectMap.keySet().iterator().next(); + if (objectMap.get(fieldName) == null) { - String msg = I18n.i18n(I18n.BundleType.Execution, locale) - .msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName); - throw new OneOfNullValueException(msg); + throwValueIsNullError(oneOfInputType, locale, fieldName); } } + + private static void throwValueIsNullError(GraphQLInputObjectType oneOfInputType, Locale locale, String fieldName) { + String msg = I18n.i18n(I18n.BundleType.Execution, locale) + .msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName); + throw new OneOfNullValueException(msg); + } + + private static void throwNotOneFieldError(GraphQLInputObjectType oneOfInputType, Locale locale) { + String msg = I18n.i18n(I18n.BundleType.Execution, locale) + .msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName()); + throw new OneOfTooManyKeysException(msg); + } } diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 8e40ac4433..83488658d2 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -600,6 +600,9 @@ class ValuesResolverTest extends Specification { a: VariableReference.of("var") ]) | CoercedVariables.of(["var": null]) + '`{ a: $var }` { }' | buildObjectLiteral([ + a: VariableReference.of("var") + ]) | CoercedVariables.emptyVariables() } def "getArgumentValues: invalid oneOf list input because element contains duplicate key - #testCase"() { From f91244054ea5cf09744427996ddcb077a69ad01f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 2 Feb 2024 16:09:15 +1100 Subject: [PATCH 121/393] Fix all unit tests and start cleaning up --- .../execution/AsyncExecutionStrategy.java | 3 +- .../graphql/execution/defer/DeferredCall.java | 4 - .../DeferredFieldInstrumentationContext.java | 2 - .../FieldLevelTrackingApproach.java | 18 -- .../defer/CapturingSubscriber.groovy | 53 ------ .../defer/IncrementalContextDeferTest.groovy | 28 +-- ...eferExecutionSupportIntegrationTest.groovy | 16 +- .../dataloader/BatchCompareDataFetchers.java | 4 - .../DataLoaderPerformanceData.groovy | 174 +++++++++++++----- .../DataLoaderPerformanceTest.groovy | 63 +++---- ...manceWithChainedInstrumentationTest.groovy | 60 +++--- .../execution/pubsub/CapturingSubscriber.java | 5 + .../SubscriptionUniqueRootFieldTest.groovy | 13 +- 13 files changed, 218 insertions(+), 225 deletions(-) delete mode 100644 src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 58bcb4e5ac..22575b0e6a 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -213,7 +213,7 @@ public Set createCalls() { Instrumentation instrumentation = executionContext.getInstrumentation(); DeferredFieldInstrumentationContext fieldCtx = instrumentation.beginDeferredField( - new InstrumentationDeferredFieldParameters(executionContext, parameters), executionContext.getInstrumentationState() + new InstrumentationDeferredFieldParameters(executionContext, callParameters), executionContext.getInstrumentationState() ); @@ -228,7 +228,6 @@ public Set createCalls() { return fieldValueResult .thenApply(fieldValueInfo -> { - System.out.println("then apply: " + fieldValueInfo.getFieldValue().isDone()); fieldCtx.onFieldValueInfo(fieldValueInfo); return fieldValueInfo; }) diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index 88b1a31ed2..4b4d9905f7 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -8,7 +8,6 @@ import graphql.execution.NonNullableFieldWasNullError; import graphql.execution.NonNullableFieldWasNullException; import graphql.execution.ResultPath; -import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.incremental.DeferPayload; import java.util.Collections; @@ -63,7 +62,6 @@ public DeferredCall( @Override public CompletableFuture invoke() { - System.out.println("DeferredCall.invoke(): " + this.getPath()); Async.CombinedBuilder futures = Async.ofExpectedSize(calls.size()); calls.forEach(call -> { @@ -83,7 +81,6 @@ public CompletableFuture invoke() { * and build a special {@link DeferPayload} that captures the details of the error. */ private DeferPayload handleNonNullableFieldError(DeferPayload result, Throwable throwable) { - System.out.println("DeferredCall.handleNonNullableFieldError(): " + this.getPath()); if (throwable != null) { Throwable cause = throwable.getCause(); if (cause instanceof NonNullableFieldWasNullException) { @@ -103,7 +100,6 @@ private DeferPayload handleNonNullableFieldError(DeferPayload result, Throwable } private DeferPayload transformToDeferredPayload(List fieldWithExecutionResults) { - System.out.println("DeferredCall.transformToDeferredPayload(): " + this.getPath()); List errorsEncountered = deferredCallContext.getErrors(); Map dataMap = new HashMap<>(); diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java index d700f3c58a..b516c9e88e 100644 --- a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java @@ -6,8 +6,6 @@ public interface DeferredFieldInstrumentationContext extends InstrumentationContext { default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - System.out.println("DeferredFieldInstrumentationContext.onFieldValueInfo() [default]: " + fieldValueInfo); - } } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 4c8865a967..1207848a95 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -17,7 +17,6 @@ import org.dataloader.DataLoaderRegistry; import org.slf4j.Logger; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -203,28 +202,13 @@ public void onDispatched(CompletableFuture result) { return dispatchIfNeeded(callStack, level); }); if (dispatchNeeded) { - // TODO: I think "dispatch" is being called before it should - System.out.println("InstrumentationContext.onDispatched(): dispatching"); dispatch(); } } diff --git a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy b/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy deleted file mode 100644 index ec9f824090..0000000000 --- a/src/test/groovy/graphql/execution/defer/CapturingSubscriber.groovy +++ /dev/null @@ -1,53 +0,0 @@ -package graphql.execution.defer - - -import graphql.incremental.DeferPayload -import graphql.incremental.DelayedIncrementalExecutionResult -import org.reactivestreams.Publisher -import org.reactivestreams.Subscriber -import org.reactivestreams.Subscription - -import java.util.concurrent.atomic.AtomicBoolean - -class CapturingSubscriber implements Subscriber { - Subscription subscription - AtomicBoolean finished = new AtomicBoolean() - Throwable throwable - List executionResults = [] - List executionResultData = [] - - AtomicBoolean subscribeTo(Publisher publisher) { - publisher.subscribe(this) - return finished - } - - @Override - void onSubscribe(Subscription s) { - assert s != null, "subscription must not be null" - this.subscription = s - s.request(1) - } - - @Override - void onNext(DelayedIncrementalExecutionResult incrementalExecutionResult) { - executionResults.add(incrementalExecutionResult) - executionResultData.addAll(incrementalExecutionResult.incremental - // We only support defer (and not stream) for now - .collect {((DeferPayload) it).data} - ) - subscription.request(1) - } - - @Override - void onError(Throwable t) { - println "Publisher emitted an error" - t.printStackTrace() - throwable = t - finished.set(true) - } - - @Override - void onComplete() { - finished.set(true) - } -} diff --git a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy index 802fb0e951..c0ff24abed 100644 --- a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy +++ b/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy @@ -57,7 +57,7 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) when: - def subscriber = new CapturingSubscriber() { + def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { @Override void onComplete() { assert false, "This should not be called!" @@ -65,10 +65,10 @@ class IncrementalContextDeferTest extends Specification { } incrementalContext.startDeferredCalls().subscribe(subscriber) - Awaitility.await().untilTrue(subscriber.finished) + Awaitility.await().untilTrue(subscriber.isDone()) - def results = subscriber.executionResults - def thrown = subscriber.throwable + def results = subscriber.getEvents() + def thrown = subscriber.getThrowable() then: thrown.message == "java.lang.RuntimeException: Bang" @@ -83,18 +83,18 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - def subscriber = new CapturingSubscriber() { + def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { @Override void onNext(DelayedIncrementalExecutionResult executionResult) { - this.executionResults.add(executionResult) + this.getEvents().add(executionResult) subscription.cancel() - finished.set(true) + this.isDone().set(true) } } incrementalContext.startDeferredCalls().subscribe(subscriber) - Awaitility.await().untilTrue(subscriber.finished) - def results = subscriber.executionResults + Awaitility.await().untilTrue(subscriber.isDone()) + def results = subscriber.getEvents() then: results.size() == 1 @@ -112,8 +112,8 @@ class IncrementalContextDeferTest extends Specification { incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - def subscriber1 = new CapturingSubscriber() - def subscriber2 = new CapturingSubscriber() + def subscriber1 = new graphql.execution.pubsub.CapturingSubscriber() + def subscriber2 = new graphql.execution.pubsub.CapturingSubscriber() incrementalContext.startDeferredCalls().subscribe(subscriber1) incrementalContext.startDeferredCalls().subscribe(subscriber2) @@ -239,11 +239,11 @@ class IncrementalContextDeferTest extends Specification { } private static List startAndWaitCalls(IncrementalContext incrementalContext) { - def subscriber = new CapturingSubscriber() + def subscriber = new graphql.execution.pubsub.CapturingSubscriber() incrementalContext.startDeferredCalls().subscribe(subscriber) - Awaitility.await().untilTrue(subscriber.finished) - return subscriber.executionResults + Awaitility.await().untilTrue(subscriber.isDone()) + return subscriber.getEvents() } } diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 0d98553d06..89d9ef7ede 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -5,7 +5,7 @@ import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQL import graphql.TestUtil -import graphql.execution.defer.CapturingSubscriber +import graphql.execution.pubsub.CapturingSubscriber import graphql.incremental.DelayedIncrementalExecutionResult import graphql.incremental.IncrementalExecutionResult import graphql.incremental.IncrementalExecutionResultImpl @@ -86,13 +86,13 @@ class DeferExecutionSupportIntegrationTest extends Specification { private static DataFetcher resolve(Object value, Integer sleepMs, boolean allowMultipleCalls) { return new DataFetcher() { - boolean executed = false; + boolean executed = false @Override Object get(DataFetchingEnvironment environment) throws Exception { if(executed && !allowMultipleCalls) { throw new IllegalStateException("This data fetcher can run only once") } - executed = true; + executed = true return CompletableFuture.supplyAsync { Thread.sleep(sleepMs) return value @@ -1358,11 +1358,13 @@ class DeferExecutionSupportIntegrationTest extends Specification { private static List> getIncrementalResults(IncrementalExecutionResult initialResult) { Publisher deferredResultStream = initialResult.incrementalItemPublisher - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream) - Awaitility.await().untilTrue(subscriber.finished) + def subscriber = new CapturingSubscriber() - return subscriber.executionResults + deferredResultStream.subscribe(subscriber) + + Awaitility.await().untilTrue(subscriber.isDone()) + + return subscriber.getEvents() .collect { it.toSpecification() } } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java b/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java index 430f79a74c..7f20938b3b 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java @@ -77,15 +77,12 @@ private static List getDepartmentsForShop(Shop shop) { } private static List> getDepartmentsForShops(List shops) { - System.out.println("getDepartmentsForShops batch: " + shops); List> departmentsResult = shops.stream().map(BatchCompareDataFetchers::getDepartmentsForShop).collect(Collectors.toList()); - System.out.println("result " + departmentsResult); return departmentsResult; } private BatchLoader> departmentsForShopsBatchLoader = ids -> maybeAsyncWithSleep(() -> { - System.out.println("ids" + ids); departmentsForShopsBatchLoaderCounter.incrementAndGet(); List shopList = new ArrayList<>(); for (String id : ids) { @@ -127,7 +124,6 @@ private static List getProductsForDepartment(Department department) { } private static List> getProductsForDepartments(List departments) { - System.out.println("getProductsForDepartments batch: " + departments); return departments.stream().map(BatchCompareDataFetchers::getProductsForDepartment).collect(Collectors.toList()); } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy index 5ec34abc24..4de47ebcbd 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy @@ -3,9 +3,13 @@ package graphql.execution.instrumentation.dataloader import graphql.Directives import graphql.GraphQL import graphql.execution.instrumentation.Instrumentation +import graphql.execution.pubsub.CapturingSubscriber +import graphql.incremental.DelayedIncrementalExecutionResult +import graphql.incremental.IncrementalExecutionResult import graphql.schema.GraphQLSchema +import org.awaitility.Awaitility import org.dataloader.DataLoaderRegistry - +import org.reactivestreams.Publisher class DataLoaderPerformanceData { @@ -109,6 +113,46 @@ class DataLoaderPerformanceData { ] + static void assertIncrementalExpensiveData(List> incrementalResults) { + // Ordering is non-deterministic, so we assert on the things we know are going to be true. + + assert incrementalResults.size() == 25 + // only the last payload has "hasNext=true" + assert incrementalResults.subList(0, 24).every { it.hasNext == true } + assert incrementalResults[24].hasNext == false + + // every payload has only 1 incremental item, and the data is the same for all of them + assert incrementalResults.every { it.incremental.size == 1 } + + def incrementalResultsItems = incrementalResults.collect { it.incremental[0] } + + // the order of the actual data is non-deterministic. So we assert via "any" that the data is there + assert incrementalResultsItems.any { it == [path: ["shops", 0], data: [departments: [[name: "Department 1"], [name: "Department 2"], [name: "Department 3"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 0], data: [expensiveDepartments: [[name: "Department 1", products: [[name: "Product 1"]], expensiveProducts: [[name: "Product 1"]]], [name: "Department 2", products: [[name: "Product 2"]], expensiveProducts: [[name: "Product 2"]]], [name: "Department 3", products: [[name: "Product 3"]], expensiveProducts: [[name: "Product 3"]]]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1], data: [expensiveDepartments: [[name: "Department 4", products: [[name: "Product 4"]], expensiveProducts: [[name: "Product 4"]]], [name: "Department 5", products: [[name: "Product 5"]], expensiveProducts: [[name: "Product 5"]]], [name: "Department 6", products: [[name: "Product 6"]], expensiveProducts: [[name: "Product 6"]]]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1], data: [departments: [[name: "Department 4"], [name: "Department 5"], [name: "Department 6"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2], data: [departments: [[name: "Department 7"], [name: "Department 8"], [name: "Department 9"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2], data: [expensiveDepartments: [[name: "Department 7", products: [[name: "Product 7"]], expensiveProducts: [[name: "Product 7"]]], [name: "Department 8", products: [[name: "Product 8"]], expensiveProducts: [[name: "Product 8"]]], [name: "Department 9", products: [[name: "Product 9"]], expensiveProducts: [[name: "Product 9"]]]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 0, "departments", 0], data: [products: [[name: "Product 1"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 0, "departments", 0], data: [expensiveProducts: [[name: "Product 1"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 0, "departments", 1], data: [products: [[name: "Product 2"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 0, "departments", 1], data: [expensiveProducts: [[name: "Product 2"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 0, "departments", 2], data: [products: [[name: "Product 3"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 0, "departments", 2], data: [expensiveProducts: [[name: "Product 3"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1, "departments", 0], data: [expensiveProducts: [[name: "Product 4"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1, "departments", 0], data: [products: [[name: "Product 4"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1, "departments", 1], data: [products: [[name: "Product 5"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1, "departments", 1], data: [expensiveProducts: [[name: "Product 5"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1, "departments", 2], data: [expensiveProducts: [[name: "Product 6"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 1, "departments", 2], data: [products: [[name: "Product 6"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2, "departments", 0], data: [products: [[name: "Product 7"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2, "departments", 0], data: [expensiveProducts: [[name: "Product 7"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2, "departments", 1], data: [products: [[name: "Product 8"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2, "departments", 1], data: [expensiveProducts: [[name: "Product 8"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2, "departments", 2], data: [products: [[name: "Product 9"]]]] } + assert incrementalResultsItems.any { it == [path: ["shops", 2, "departments", 2], data: [expensiveProducts: [[name: "Product 9"]]]] } + assert incrementalResultsItems.any { it == [path: [], data: [expensiveShops: [[id: "exshop-1", name: "ExShop 1"], [id: "exshop-2", name: "ExShop 2"], [id: "exshop-3", name: "ExShop 3"]]]] } + } static def expensiveQuery = """ query { @@ -158,37 +202,56 @@ class DataLoaderPerformanceData { """ static def expectedInitialDeferredData = [ - shops: [ - [id: "shop-1", name: "Shop 1", departments: null], - [id: "shop-2", name: "Shop 2", departments: null], - [id: "shop-3", name: "Shop 3", departments: null], - ] - ] - - static def expectedInitialExpensiveDeferredData = [ - shops : [ - [id: "shop-1", name: "Shop 1", departments: null, expensiveDepartments: null], - [id: "shop-2", name: "Shop 2", departments: null, expensiveDepartments: null], - [id: "shop-3", name: "Shop 3", departments: null, expensiveDepartments: null], + data : [ + shops: [ + [id: "shop-1", name: "Shop 1"], + [id: "shop-2", name: "Shop 2"], + [id: "shop-3", name: "Shop 3"], + ] ], - expensiveShops: null + hasNext: true ] static def expectedListOfDeferredData = [ - [[id: "department-1", name: "Department 1", products: [[id: "product-1", name: "Product 1"]]], - [id: "department-2", name: "Department 2", products: [[id: "product-2", name: "Product 2"]]], - [id: "department-3", name: "Department 3", products: [[id: "product-3", name: "Product 3"]]]] - , - - [[id: "department-4", name: "Department 4", products: [[id: "product-4", name: "Product 4"]]], - [id: "department-5", name: "Department 5", products: [[id: "product-5", name: "Product 5"]]], - [id: "department-6", name: "Department 6", products: [[id: "product-6", name: "Product 6"]]]] - , - [[id: "department-7", name: "Department 7", products: [[id: "product-7", name: "Product 7"]]], - [id: "department-8", name: "Department 8", products: [[id: "product-8", name: "Product 8"]]], - [id: "department-9", name: "Department 9", products: [[id: "product-9", name: "Product 9"]]]] - , - + [ + hasNext : true, + incremental: [[ + path: ["shops", 0], + data: [ + departments: [ + [id: "department-1", name: "Department 1", products: [[id: "product-1", name: "Product 1"]]], + [id: "department-2", name: "Department 2", products: [[id: "product-2", name: "Product 2"]]], + [id: "department-3", name: "Department 3", products: [[id: "product-3", name: "Product 3"]]] + ] + ] + ]], + ], + [ + hasNext : true, + incremental: [[ + path: ["shops", 1], + data: [ + departments: [ + [id: "department-4", name: "Department 4", products: [[id: "product-4", name: "Product 4"]]], + [id: "department-5", name: "Department 5", products: [[id: "product-5", name: "Product 5"]]], + [id: "department-6", name: "Department 6", products: [[id: "product-6", name: "Product 6"]]] + ] + ], + ]], + ], + [ + hasNext : false, + incremental: [[ + path: ["shops", 2], + data: [ + departments: [ + [id: "department-7", name: "Department 7", products: [[id: "product-7", name: "Product 7"]]], + [id: "department-8", name: "Department 8", products: [[id: "product-8", name: "Product 8"]]], + [id: "department-9", name: "Department 9", products: [[id: "product-9", name: "Product 9"]]] + ] + ] + ]], + ] ] @@ -212,28 +275,38 @@ class DataLoaderPerformanceData { query { shops { id name - departments @defer { - name - products @defer { - name - } - expensiveProducts @defer { - name - } - } - expensiveDepartments @defer { - name - products { + ... @defer { + departments { name + ... @defer { + products { + name + } + } + ... @defer { + expensiveProducts { + name + } + } } - expensiveProducts { + } + ... @defer { + expensiveDepartments { name + products { + name + } + expensiveProducts { + name + } } - } - } - expensiveShops @defer { - id name + } } + ... @defer { + expensiveShops { + id name + } + } } """ @@ -264,4 +337,15 @@ class DataLoaderPerformanceData { [[name: "Product 9"]], [[name: "Product 9"]], ] + + static List> getIncrementalResults(IncrementalExecutionResult initialResult) { + Publisher deferredResultStream = initialResult.incrementalItemPublisher + + def subscriber = new CapturingSubscriber() + deferredResultStream.subscribe(subscriber) + Awaitility.await().untilTrue(subscriber.isDone()) + + return subscriber.getEvents() + .collect { it.toSpecification() } + } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index d62732eb81..d2a979147e 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -2,24 +2,20 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput import graphql.GraphQL -import graphql.execution.defer.CapturingSubscriber import graphql.execution.instrumentation.Instrumentation -import graphql.incremental.DelayedIncrementalExecutionResult import graphql.incremental.IncrementalExecutionResult -import org.awaitility.Awaitility import org.dataloader.DataLoaderRegistry -import org.reactivestreams.Publisher import spock.lang.Specification +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.assertIncrementalExpensiveData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedExpensiveData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialDeferredData -import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialExpensiveDeferredData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedData -import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveData -import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveDeferredData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedListOfDeferredData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveQuery +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getIncrementalResults import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getQuery class DataLoaderPerformanceTest extends Specification { @@ -101,24 +97,24 @@ class DataLoaderPerformanceTest extends Specification { when: - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(deferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() - def result = graphQL.execute(executionInput) - - Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher - - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream) - Awaitility.await().untilTrue(subscriber.finished) + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(deferredQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(true) + .build() + IncrementalExecutionResult result = graphQL.execute(executionInput) then: + result.toSpecification() == expectedInitialDeferredData - result.data == expectedInitialDeferredData + when: + def incrementalResults = getIncrementalResults(result) - subscriber.executionResultData == expectedListOfDeferredData + then: + incrementalResults == expectedListOfDeferredData - // - // with deferred results, we don't achieve the same efficiency + // With deferred results, we don't achieve the same efficiency. batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 } @@ -127,25 +123,26 @@ class DataLoaderPerformanceTest extends Specification { when: - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveDeferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() - def result = graphQL.execute(executionInput) - - Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher - - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream) - Awaitility.await().untilTrue(subscriber.finished) + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(expensiveDeferredQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(true) + .build() + IncrementalExecutionResult result = graphQL.execute(executionInput) then: + result.toSpecification() == expectedInitialDeferredData - result.data == expectedInitialExpensiveDeferredData + when: + def incrementalResults = getIncrementalResults(result) - subscriber.executionResultData == expectedExpensiveDeferredData + then: + assertIncrementalExpensiveData(incrementalResults) - // - // with deferred results, we don't achieve the same efficiency - batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 - batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 + // With deferred results, we don't achieve the same efficiency. + // The final number of loader calls is non-deterministic, so we can't assert an exact number. + batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() >= 3 + batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() >= 3 } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 897dc32edb..762018b0d5 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -2,26 +2,22 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput import graphql.GraphQL -import graphql.execution.defer.CapturingSubscriber import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation -import graphql.incremental.DelayedIncrementalExecutionResult import graphql.incremental.IncrementalExecutionResult -import org.awaitility.Awaitility import org.dataloader.DataLoaderRegistry -import org.reactivestreams.Publisher import spock.lang.Ignore import spock.lang.Specification +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.assertIncrementalExpensiveData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedExpensiveData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialDeferredData -import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialExpensiveDeferredData +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedListOfDeferredData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedData -import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveData -import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedExpensiveDeferredData -import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpectedListOfDeferredData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveDeferredQuery import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getExpensiveQuery +import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getIncrementalResults import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.getQuery class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification { @@ -115,49 +111,47 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification .dataLoaderRegistry(dataLoaderRegistry) .incrementalSupport(true) .build() - def result = graphQL.execute(executionInput) - Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher + IncrementalExecutionResult result = graphQL.execute(executionInput) - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream) - Awaitility.await().untilTrue(subscriber.finished) + then: + result.toSpecification() == expectedInitialDeferredData + when: + def incrementalResults = getIncrementalResults(result) then: + incrementalResults == expectedListOfDeferredData - result.data == expectedInitialDeferredData - subscriber.executionResultData == expectedListOfDeferredData - - // - // with deferred results, we don't achieve the same efficiency + // With deferred results, we don't achieve the same efficiency. batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 } - def "chainedInstrumentation: data loader will work with deferred queries on multiple levels deep"() { + def "chainedInstrumentation: data loader will work with deferred queries on multiple levels deep"() { when: + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .incrementalSupport(true) + .query(expensiveDeferredQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .build() - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveDeferredQuery).dataLoaderRegistry(dataLoaderRegistry).build() - def result = graphQL.execute(executionInput) + IncrementalExecutionResult result = graphQL.execute(executionInput) - Publisher deferredResultStream = ((IncrementalExecutionResult) result).incrementalItemPublisher + then: + result.toSpecification() == expectedInitialDeferredData - def subscriber = new CapturingSubscriber() - subscriber.subscribeTo(deferredResultStream) - Awaitility.await().untilTrue(subscriber.finished) + when: + def incrementalResults = getIncrementalResults(result) then: + assertIncrementalExpensiveData(incrementalResults) - result.data == expectedInitialExpensiveDeferredData - - subscriber.executionResultData == expectedExpensiveDeferredData - - // - // with deferred results, we don't achieve the same efficiency - batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 3 - batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 + // With deferred results, we don't achieve the same efficiency. + // The final number of loader calls is non-deterministic, so we can't assert an exact number. + batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() >= 3 + batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() >= 3 } } diff --git a/src/test/groovy/graphql/execution/pubsub/CapturingSubscriber.java b/src/test/groovy/graphql/execution/pubsub/CapturingSubscriber.java index e6c3de62d4..f736807941 100644 --- a/src/test/groovy/graphql/execution/pubsub/CapturingSubscriber.java +++ b/src/test/groovy/graphql/execution/pubsub/CapturingSubscriber.java @@ -55,4 +55,9 @@ public Throwable getThrowable() { public AtomicBoolean isDone() { return done; } + + public Subscription getSubscription() { + return subscription; + } + } diff --git a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy b/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy index f37e683c05..1e2d72756f 100644 --- a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy +++ b/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy @@ -12,15 +12,8 @@ class SubscriptionUniqueRootFieldTest extends Specification { given: def subscriptionOneRoot = ''' subscription doggo { - ... { - dog { - name - } - ... { - dog2: dog { - name - } - } + dog { + name } } ''' @@ -29,7 +22,7 @@ class SubscriptionUniqueRootFieldTest extends Specification { def validationErrors = validate(subscriptionOneRoot) then: - !validationErrors.empty + validationErrors.empty } def "5.2.3.1 subscription with only one root field with fragment passes validation"() { From 28d98d4bdcea804423a5e1a6f071a33182a7a1f5 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 2 Feb 2024 17:24:23 +1100 Subject: [PATCH 122/393] Simplify instrumentation code for deferred field execution --- .../execution/AsyncExecutionStrategy.java | 25 ++++++++++--------- .../graphql/execution/defer/DeferredCall.java | 4 +++ .../ChainedInstrumentation.java | 18 +++++-------- .../DeferredFieldInstrumentationContext.java | 11 -------- .../instrumentation/Instrumentation.java | 21 +++++----------- .../DataLoaderDispatcherInstrumentation.java | 10 +++----- .../FieldLevelTrackingApproach.java | 10 +++----- 7 files changed, 37 insertions(+), 62 deletions(-) delete mode 100644 src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 22575b0e6a..5dd2e36360 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -8,9 +8,9 @@ import graphql.execution.defer.DeferredCall; import graphql.execution.defer.DeferredCallContext; import graphql.execution.incremental.DeferExecution; -import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.util.FpKit; @@ -212,11 +212,10 @@ public Set createCalls() { Instrumentation instrumentation = executionContext.getInstrumentation(); - DeferredFieldInstrumentationContext fieldCtx = instrumentation.beginDeferredField( + InstrumentationContext fieldCtx = instrumentation.beginDeferredField( new InstrumentationDeferredFieldParameters(executionContext, callParameters), executionContext.getInstrumentationState() ); - return dfCache.computeIfAbsent( currentField.getName(), // The same field can be associated with multiple defer executions, so @@ -224,17 +223,19 @@ public Set createCalls() { key -> FpKit.interThreadMemoize(() -> { CompletableFuture fieldValueResult = resolveFieldWithInfoFn.apply(executionContext, callParameters); - fieldCtx.onDispatched(null); + CompletableFuture executionResultCF = fieldValueResult + .thenCompose(FieldValueInfo::getFieldValue); + + fieldCtx.onDispatched(executionResultCF); - return fieldValueResult - .thenApply(fieldValueInfo -> { - fieldCtx.onFieldValueInfo(fieldValueInfo); - return fieldValueInfo; - }) - .thenCompose(FieldValueInfo::getFieldValue) - .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult)); + return executionResultCF + .thenApply(executionResult -> + new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult) + ) + .whenComplete((fieldWithExecutionResult, throwable) -> + fieldCtx.onCompleted(fieldWithExecutionResult.getExecutionResult(), throwable) + ); } -// .whenComplete(fieldCtx::onCompleted) ) ); diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java index 4b4d9905f7..f448622155 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -127,5 +127,9 @@ public FieldWithExecutionResult(String fieldName, ExecutionResult executionResul this.fieldName = fieldName; this.executionResult = executionResult; } + + public ExecutionResult getExecutionResult() { + return executionResult; + } } } diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index ca475bca1a..886573b96f 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -9,7 +9,6 @@ import graphql.execution.Async; import graphql.execution.ExecutionContext; import graphql.execution.FieldValueInfo; -import graphql.execution.defer.DeferredCall; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; @@ -180,7 +179,7 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument } @Override - public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState instrumentationState) { + public InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState instrumentationState) { return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() .map(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, instrumentationState); @@ -449,28 +448,23 @@ public void onFieldValuesException() { } } - private static class ChainedDeferredExecutionStrategyInstrumentationContext implements DeferredFieldInstrumentationContext { + private static class ChainedDeferredExecutionStrategyInstrumentationContext implements InstrumentationContext { - private final List contexts; + private final List> contexts; - ChainedDeferredExecutionStrategyInstrumentationContext(List contexts) { + ChainedDeferredExecutionStrategyInstrumentationContext(List> contexts) { this.contexts = Collections.unmodifiableList(contexts); } @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched(CompletableFuture result) { contexts.forEach(context -> context.onDispatched(result)); } @Override - public void onCompleted(DeferredCall.FieldWithExecutionResult result, Throwable t) { + public void onCompleted(ExecutionResult result, Throwable t) { contexts.forEach(context -> context.onCompleted(result, t)); } - - @Override - public void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - contexts.forEach(context -> context.onFieldValueInfo(fieldValueInfo)); - } } } diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java deleted file mode 100644 index b516c9e88e..0000000000 --- a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package graphql.execution.instrumentation; - -import graphql.execution.FieldValueInfo; -import graphql.execution.defer.DeferredCall; - -public interface DeferredFieldInstrumentationContext extends InstrumentationContext { - - default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - } - -} diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index f6c70fd0e8..9445530922 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -4,7 +4,6 @@ import graphql.ExecutionResult; import graphql.PublicSpi; import graphql.execution.ExecutionContext; -import graphql.execution.defer.DeferredCall; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; @@ -225,21 +224,13 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen } /** - * TODO Javadoc - * @return + * This is called just before a deferred field is resolved into a value + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @return a non-null {@link InstrumentationContext} object that will be called back when the step ends */ - default DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState instrumentationState) { - return new DeferredFieldInstrumentationContext() { - @Override - public void onDispatched(CompletableFuture result) { - - } - - @Override - public void onCompleted(DeferredCall.FieldWithExecutionResult result, Throwable t) { - - } - }; + default InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { + return noOp(); } /** diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index b1aa63247d..1e9fe811b1 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -7,8 +7,6 @@ import graphql.execution.AsyncExecutionStrategy; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategy; -import graphql.execution.defer.DeferredCall; -import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -138,19 +136,19 @@ private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContex } @Override - public DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState rawState) { + public InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState rawState) { DataLoaderDispatcherInstrumentationState state = ofState(rawState); // // if there are no data loaders, there is nothing to do // if (state.hasNoDataLoaders()) { - return new DeferredFieldInstrumentationContext() { + return new InstrumentationContext<>() { @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched(CompletableFuture result) { } @Override - public void onCompleted(DeferredCall.FieldWithExecutionResult result, Throwable t) { + public void onCompleted(ExecutionResult result, Throwable t) { } }; diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 1207848a95..8a35653730 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -5,8 +5,6 @@ import graphql.Internal; import graphql.execution.FieldValueInfo; import graphql.execution.ResultPath; -import graphql.execution.defer.DeferredCall; -import graphql.execution.instrumentation.DeferredFieldInstrumentationContext; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -187,16 +185,16 @@ private int getCountForList(List fieldValueInfos) { return result; } - DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { + InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { CallStack callStack = new CallStack(); int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); synchronized (callStack) { callStack.clearAndMarkCurrentLevelAsReady(level); } - return new DeferredFieldInstrumentationContext() { + return new InstrumentationContext<>() { @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched(CompletableFuture result) { boolean dispatchNeeded = callStack.lock.callLocked(() -> { callStack.increaseFetchCount(level); return dispatchIfNeeded(callStack, level); @@ -207,7 +205,7 @@ public void onDispatched(CompletableFuture Date: Fri, 2 Feb 2024 18:37:02 +1100 Subject: [PATCH 123/393] Rename some things defer -> incremental --- src/main/java/graphql/execution/Async.java | 10 - .../execution/AsyncExecutionStrategy.java | 171 ++++++++++-------- .../java/graphql/execution/Execution.java | 8 +- .../graphql/execution/ExecutionContext.java | 2 +- .../graphql/execution/ExecutionStrategy.java | 8 +- .../ExecutionStrategyParameters.java | 22 +-- .../{defer => incremental}/DeferredCall.java | 6 +- .../DeferredCallContext.java | 2 +- .../IncrementalCall.java | 2 +- .../IncrementalContext.java | 24 +-- .../{defer => incremental}/StreamedCall.java | 2 +- .../DeferredCallTest.groovy | 4 +- .../IncrementalContextDeferTest.groovy | 9 +- .../DataLoaderPerformanceTest.groovy | 37 +++- ...manceWithChainedInstrumentationTest.groovy | 35 +++- 15 files changed, 206 insertions(+), 136 deletions(-) rename src/main/java/graphql/execution/{defer => incremental}/DeferredCall.java (96%) rename src/main/java/graphql/execution/{defer => incremental}/DeferredCallContext.java (97%) rename src/main/java/graphql/execution/{defer => incremental}/IncrementalCall.java (90%) rename src/main/java/graphql/execution/{defer => incremental}/IncrementalContext.java (83%) rename src/main/java/graphql/execution/{defer => incremental}/StreamedCall.java (93%) rename src/test/groovy/graphql/execution/{defer => incremental}/DeferredCallTest.groovy (96%) rename src/test/groovy/graphql/execution/{defer => incremental}/IncrementalContextDeferTest.groovy (96%) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index f76130fc98..56f7a2f9be 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -21,16 +21,6 @@ @SuppressWarnings("FutureReturnValueIgnored") public class Async { - public static void copyResults(CompletableFuture source, CompletableFuture target) { - source.whenComplete((o, throwable) -> { - if (throwable != null) { - target.completeExceptionally(throwable); - return; - } - target.complete(o); - }); - } - public interface CombinedBuilder { void add(CompletableFuture completableFuture); diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 5dd2e36360..47f2673e54 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -5,14 +5,16 @@ import com.google.common.collect.ImmutableSet; import graphql.ExecutionResult; import graphql.PublicApi; -import graphql.execution.defer.DeferredCall; -import graphql.execution.defer.DeferredCallContext; +import graphql.execution.incremental.DeferredCall; +import graphql.execution.incremental.DeferredCallContext; +import graphql.execution.incremental.IncrementalCall; import graphql.execution.incremental.DeferExecution; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.incremental.IncrementalPayload; import graphql.util.FpKit; import java.util.Collections; @@ -72,7 +74,9 @@ public CompletableFuture execute(ExecutionContext executionCont executionContext.getIncrementalContext().enqueue(deferExecutionSupport.createCalls()); - Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size() - deferExecutionSupport.deferredFieldsCount()); + // Only non-deferred fields should be considered for calculating the expected size of futures. + Async.CombinedBuilder futures = Async + .ofExpectedSize(fields.size() - deferExecutionSupport.deferredFieldsCount()); for (String fieldName : fieldNames) { MergedField currentField = fields.getSubField(fieldName); @@ -94,9 +98,9 @@ public CompletableFuture execute(ExecutionContext executionCont executionStrategyCtx.onDispatched(overallResult); futures.await().whenComplete((completeValueInfos, throwable) -> { - List fieldsExecutedInInitialResult = deferExecutionSupport.getNonDeferredFieldNames(fieldNames); + List fieldsExecutedOnInitialResult = deferExecutionSupport.getNonDeferredFieldNames(fieldNames); - BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldsExecutedInInitialResult, overallResult); + BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldsExecutedOnInitialResult, overallResult); if (throwable != null) { handleResultsConsumer.accept(null, throwable.getCause()); return; @@ -121,6 +125,15 @@ public CompletableFuture execute(ExecutionContext executionCont return overallResult; } + /** + * The purpose of this class hierarchy is to encapsulate most of the logic for deferring field execution, thus + * keeping the main execution strategy code clean and focused on the main execution logic. + *

+ * The {@link NoOp} instance should be used when incremental support is not enabled for the current execution. The + * methods in this class will return empty or no-op results, that should not impact the main execution. + *

+ * {@link DeferExecutionSupportImpl} is the actual implementation that will be used when incremental support is enabled. + */ private interface DeferExecutionSupport { boolean isDeferredField(MergedField mergedField); @@ -129,10 +142,13 @@ private interface DeferExecutionSupport { List getNonDeferredFieldNames(List allFieldNames); - Set createCalls(); + Set> createCalls(); DeferExecutionSupport NOOP = new DeferExecutionSupport.NoOp(); + /** + * An implementation that actually executes the deferred fields. + */ class DeferExecutionSupportImpl implements DeferExecutionSupport { private final ImmutableListMultimap deferExecutionToFields; private final ImmutableSet deferredFields; @@ -185,82 +201,85 @@ public int deferredFieldsCount() { public List getNonDeferredFieldNames(List allFieldNames) { return this.nonDeferredFieldNames; } - @Override - public Set createCalls() { + public Set> createCalls() { return deferExecutionToFields.keySet().stream() - .map(deferExecution -> { - DeferredCallContext errorSupport = new DeferredCallContext(); - - List mergedFields = deferExecutionToFields.get(deferExecution); - - List>> calls = mergedFields.stream() - .map(currentField -> { - Map fields = new LinkedHashMap<>(); - fields.put(currentField.getName(), currentField); - - ExecutionStrategyParameters callParameters = parameters.transform(builder -> - { - MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); - builder.deferredErrorSupport(errorSupport) - .field(currentField) - .fields(mergedSelectionSet) - .path(parameters.getPath().segment(currentField.getName())) - .parent(null); // this is a break in the parent -> child chain - it's a new start effectively - } - ); - - - Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationContext fieldCtx = instrumentation.beginDeferredField( - new InstrumentationDeferredFieldParameters(executionContext, callParameters), executionContext.getInstrumentationState() - ); - - return dfCache.computeIfAbsent( - currentField.getName(), - // The same field can be associated with multiple defer executions, so - // we memoize the field resolution to avoid multiple calls to the same data fetcher - key -> FpKit.interThreadMemoize(() -> { - CompletableFuture fieldValueResult = resolveFieldWithInfoFn.apply(executionContext, callParameters); - - CompletableFuture executionResultCF = fieldValueResult - .thenCompose(FieldValueInfo::getFieldValue); - - fieldCtx.onDispatched(executionResultCF); - - return executionResultCF - .thenApply(executionResult -> - new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult) - ) - .whenComplete((fieldWithExecutionResult, throwable) -> - fieldCtx.onCompleted(fieldWithExecutionResult.getExecutionResult(), throwable) - ); - } - ) - ); - - }) - .collect(Collectors.toList()); - - // with a deferred field we are really resetting where we execute from, that is from this current field onwards - return new DeferredCall( - deferExecution.getLabel(), - this.parameters.getPath(), - calls, - errorSupport - ); - }) + .map(this::createDeferredCall) .collect(Collectors.toSet()); } -// private ExecutionStepInfo createSubscribedFieldStepInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { -// Field field = parameters.getField().getSingleField(); -// GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); -// GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field); -// return createExecutionStepInfo(executionContext, parameters, fieldDef, parentType); -// } + private DeferredCall createDeferredCall(DeferExecution deferExecution) { + DeferredCallContext deferredCallContext = new DeferredCallContext(); + + List mergedFields = deferExecutionToFields.get(deferExecution); + + List>> calls = mergedFields.stream() + .map(currentField -> this.createResultSupplier(currentField, deferredCallContext)) + .collect(Collectors.toList()); + + return new DeferredCall( + deferExecution.getLabel(), + this.parameters.getPath(), + calls, + deferredCallContext + ); + } + + private Supplier> createResultSupplier( + MergedField currentField, + DeferredCallContext deferredCallContext + ) { + Map fields = new LinkedHashMap<>(); + fields.put(currentField.getName(), currentField); + + ExecutionStrategyParameters callParameters = parameters.transform(builder -> + { + MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); + builder.deferredCallContext(deferredCallContext) + .field(currentField) + .fields(mergedSelectionSet) + .path(parameters.getPath().segment(currentField.getName())) + .parent(null); // this is a break in the parent -> child chain - it's a new start effectively + } + ); + + + Instrumentation instrumentation = executionContext.getInstrumentation(); + InstrumentationContext fieldCtx = instrumentation.beginDeferredField( + new InstrumentationDeferredFieldParameters(executionContext, callParameters), executionContext.getInstrumentationState() + ); + + return dfCache.computeIfAbsent( + currentField.getName(), + // The same field can be associated with multiple defer executions, so + // we memoize the field resolution to avoid multiple calls to the same data fetcher + key -> FpKit.interThreadMemoize(() -> { + CompletableFuture fieldValueResult = resolveFieldWithInfoFn + .apply(executionContext, callParameters); + + // Create a reference to the CompletableFuture that resolves an ExecutionResult + // so we can pass it to the Instrumentation "onDispatched" callback. + CompletableFuture executionResultCF = fieldValueResult + .thenCompose(FieldValueInfo::getFieldValue); + + fieldCtx.onDispatched(executionResultCF); + + return executionResultCF + .thenApply(executionResult -> + new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult) + ) + .whenComplete((fieldWithExecutionResult, throwable) -> + fieldCtx.onCompleted(fieldWithExecutionResult.getExecutionResult(), throwable) + ); + } + ) + ); + } } + /** + * A no-op implementation that should be used when incremental support is not enabled for the current execution. + */ class NoOp implements DeferExecutionSupport { @Override @@ -279,7 +298,7 @@ public List getNonDeferredFieldNames(List allFieldNames) { } @Override - public Set createCalls() { + public Set> createCalls() { return Collections.emptySet(); } } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index b61fb8a77b..55d7eecb2e 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -7,7 +7,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; -import graphql.execution.defer.IncrementalContext; +import graphql.execution.incremental.IncrementalContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -187,11 +187,11 @@ private CompletableFuture executeOperation(ExecutionContext exe */ private CompletableFuture deferSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { - IncrementalContext deferSupport = executionContext.getIncrementalContext(); - if (deferSupport.isDeferDetected()) { + IncrementalContext incrementalContext = executionContext.getIncrementalContext(); + if (incrementalContext.getIncrementalCallsDetected()) { // we start the rest of the query now to maximize throughput. We have the initial important results, // and now we can start the rest of the calls as early as possible (even before someone subscribes) - Publisher publisher = deferSupport.startDeferredCalls(); + Publisher publisher = incrementalContext.startDeferredCalls(); return IncrementalExecutionResultImpl.fromExecutionResult(er) // "hasNext" can, in theory, be "false" when all the incremental items are delivered in the diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 1730aa9be0..d5bf2ad484 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -8,7 +8,7 @@ import graphql.GraphQLError; import graphql.PublicApi; import graphql.collect.ImmutableKit; -import graphql.execution.defer.IncrementalContext; +import graphql.execution.incremental.IncrementalContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index a7540be6bf..5a29505f96 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -363,7 +363,7 @@ protected CompletableFuture handleFetchingException( .exception(e) .build(); - parameters.deferredErrorSupport().onFetchingException(parameters, e); + parameters.getDeferredCallContext().onFetchingException(parameters, e); try { return asyncHandleException(dataFetcherExceptionHandler, handlerParameters); @@ -484,7 +484,7 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra UnresolvedTypeError error = new UnresolvedTypeError(parameters.getPath(), parameters.getExecutionStepInfo(), e); context.addError(error); - parameters.deferredErrorSupport().onError(error); + parameters.getDeferredCallContext().onError(error); } /** @@ -701,7 +701,7 @@ private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategy SerializationError error = new SerializationError(parameters.getPath(), e); context.addError(error); - parameters.deferredErrorSupport().onError(error); + parameters.getDeferredCallContext().onError(error); return null; } @@ -729,7 +729,7 @@ private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrate TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getExecutionStepInfo().getUnwrappedNonNullType()); context.addError(error); - parameters.deferredErrorSupport().onError(error); + parameters.getDeferredCallContext().onError(error); } diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index cfa76271a4..ee0cdce42e 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -2,7 +2,7 @@ import graphql.Assert; import graphql.PublicApi; -import graphql.execution.defer.DeferredCallContext; +import graphql.execution.incremental.DeferredCallContext; import java.util.function.Consumer; @@ -21,7 +21,7 @@ public class ExecutionStrategyParameters { private final ResultPath path; private final MergedField currentField; private final ExecutionStrategyParameters parent; - private final DeferredCallContext deferredErrorSupport; + private final DeferredCallContext deferredCallContext; private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, Object source, @@ -31,7 +31,7 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, ResultPath path, MergedField currentField, ExecutionStrategyParameters parent, - DeferredCallContext deferredErrorSupport) { + DeferredCallContext deferredCallContext) { this.executionStepInfo = assertNotNull(executionStepInfo, () -> "executionStepInfo is null"); this.localContext = localContext; @@ -41,7 +41,7 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, this.path = path; this.currentField = currentField; this.parent = parent; - this.deferredErrorSupport = deferredErrorSupport; + this.deferredCallContext = deferredCallContext; } public ExecutionStepInfo getExecutionStepInfo() { @@ -72,8 +72,8 @@ public ExecutionStrategyParameters getParent() { return parent; } - public DeferredCallContext deferredErrorSupport() { - return deferredErrorSupport; + public DeferredCallContext getDeferredCallContext() { + return deferredCallContext; } /** @@ -114,7 +114,7 @@ public static class Builder { ResultPath path = ResultPath.rootPath(); MergedField currentField; ExecutionStrategyParameters parent; - DeferredCallContext deferredErrorSupport = new DeferredCallContext(); + DeferredCallContext deferredCallContext = new DeferredCallContext(); /** * @see ExecutionStrategyParameters#newParameters() @@ -132,7 +132,7 @@ private Builder(ExecutionStrategyParameters oldParameters) { this.fields = oldParameters.fields; this.nonNullableFieldValidator = oldParameters.nonNullableFieldValidator; this.currentField = oldParameters.currentField; - this.deferredErrorSupport = oldParameters.deferredErrorSupport; + this.deferredCallContext = oldParameters.deferredCallContext; this.path = oldParameters.path; this.parent = oldParameters.parent; } @@ -182,13 +182,13 @@ public Builder parent(ExecutionStrategyParameters parent) { return this; } - public Builder deferredErrorSupport(DeferredCallContext deferredErrorSupport) { - this.deferredErrorSupport = deferredErrorSupport; + public Builder deferredCallContext(DeferredCallContext deferredCallContext) { + this.deferredCallContext = deferredCallContext; return this; } public ExecutionStrategyParameters build() { - return new ExecutionStrategyParameters(executionStepInfo, source, localContext, fields, nonNullableFieldValidator, path, currentField, parent, deferredErrorSupport); + return new ExecutionStrategyParameters(executionStepInfo, source, localContext, fields, nonNullableFieldValidator, path, currentField, parent, deferredCallContext); } } } diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/incremental/DeferredCall.java similarity index 96% rename from src/main/java/graphql/execution/defer/DeferredCall.java rename to src/main/java/graphql/execution/incremental/DeferredCall.java index f448622155..fce9d974f3 100644 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ b/src/main/java/graphql/execution/incremental/DeferredCall.java @@ -1,4 +1,4 @@ -package graphql.execution.defer; +package graphql.execution.incremental; import com.google.common.collect.ImmutableList; import graphql.ExecutionResult; @@ -52,12 +52,12 @@ public DeferredCall( String label, ResultPath path, List>> calls, - DeferredCallContext deferredErrorSupport + DeferredCallContext deferredCallContext ) { this.label = label; this.path = path; this.calls = calls; - this.deferredCallContext = deferredErrorSupport; + this.deferredCallContext = deferredCallContext; } @Override diff --git a/src/main/java/graphql/execution/defer/DeferredCallContext.java b/src/main/java/graphql/execution/incremental/DeferredCallContext.java similarity index 97% rename from src/main/java/graphql/execution/defer/DeferredCallContext.java rename to src/main/java/graphql/execution/incremental/DeferredCallContext.java index c9ea3dde32..0c12f426b1 100644 --- a/src/main/java/graphql/execution/defer/DeferredCallContext.java +++ b/src/main/java/graphql/execution/incremental/DeferredCallContext.java @@ -1,4 +1,4 @@ -package graphql.execution.defer; +package graphql.execution.incremental; import graphql.ExceptionWhileDataFetching; import graphql.GraphQLError; diff --git a/src/main/java/graphql/execution/defer/IncrementalCall.java b/src/main/java/graphql/execution/incremental/IncrementalCall.java similarity index 90% rename from src/main/java/graphql/execution/defer/IncrementalCall.java rename to src/main/java/graphql/execution/incremental/IncrementalCall.java index c322b0a165..7d36d48f69 100644 --- a/src/main/java/graphql/execution/defer/IncrementalCall.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCall.java @@ -1,4 +1,4 @@ -package graphql.execution.defer; +package graphql.execution.incremental; import graphql.incremental.IncrementalPayload; diff --git a/src/main/java/graphql/execution/defer/IncrementalContext.java b/src/main/java/graphql/execution/incremental/IncrementalContext.java similarity index 83% rename from src/main/java/graphql/execution/defer/IncrementalContext.java rename to src/main/java/graphql/execution/incremental/IncrementalContext.java index 02dc84fa80..5098b201ea 100644 --- a/src/main/java/graphql/execution/defer/IncrementalContext.java +++ b/src/main/java/graphql/execution/incremental/IncrementalContext.java @@ -1,4 +1,4 @@ -package graphql.execution.defer; +package graphql.execution.incremental; import graphql.Internal; import graphql.execution.reactive.SingleSubscriberPublisher; @@ -22,14 +22,14 @@ */ @Internal public class IncrementalContext { - private final AtomicBoolean deferDetected = new AtomicBoolean(false); + private final AtomicBoolean incrementalCallsDetected = new AtomicBoolean(false); private final Deque> incrementalCalls = new ConcurrentLinkedDeque<>(); private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); private final AtomicInteger pendingCalls = new AtomicInteger(); private final LockKit.ReentrantLock publisherLock = new LockKit.ReentrantLock(); @SuppressWarnings("FutureReturnValueIgnored") - private void drainDeferredCalls() { + private void drainIncrementalCalls() { IncrementalCall incrementalCall = incrementalCalls.poll(); while (incrementalCall != null) { @@ -62,29 +62,29 @@ private void drainDeferredCalls() { publisher.noMoreData(); } else { // Nested calls were added, let's try to drain the queue again. - drainDeferredCalls(); + drainIncrementalCalls(); } }); incrementalCall = incrementalCalls.poll(); } } - public void enqueue(DeferredCall deferredCall) { - deferDetected.set(true); - incrementalCalls.offer(deferredCall); + public void enqueue(IncrementalCall incrementalCall) { + incrementalCallsDetected.set(true); + incrementalCalls.offer(incrementalCall); pendingCalls.incrementAndGet(); } - public void enqueue(Collection calls) { + public void enqueue(Collection> calls) { if (!calls.isEmpty()) { - deferDetected.set(true); + incrementalCallsDetected.set(true); incrementalCalls.addAll(calls); pendingCalls.addAndGet(calls.size()); } } - public boolean isDeferDetected() { - return deferDetected.get(); + public boolean getIncrementalCallsDetected() { + return incrementalCallsDetected.get(); } /** @@ -93,7 +93,7 @@ public boolean isDeferDetected() { * @return the publisher of deferred results */ public Publisher startDeferredCalls() { - drainDeferredCalls(); + drainIncrementalCalls(); return publisher; } } diff --git a/src/main/java/graphql/execution/defer/StreamedCall.java b/src/main/java/graphql/execution/incremental/StreamedCall.java similarity index 93% rename from src/main/java/graphql/execution/defer/StreamedCall.java rename to src/main/java/graphql/execution/incremental/StreamedCall.java index 90badbc9f4..beae535b3e 100644 --- a/src/main/java/graphql/execution/defer/StreamedCall.java +++ b/src/main/java/graphql/execution/incremental/StreamedCall.java @@ -1,4 +1,4 @@ -package graphql.execution.defer; +package graphql.execution.incremental; import graphql.Internal; import graphql.incremental.StreamPayload; diff --git a/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy similarity index 96% rename from src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy rename to src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy index a05b52e569..310e70f2a2 100644 --- a/src/test/groovy/graphql/execution/defer/DeferredCallTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy @@ -1,9 +1,11 @@ -package graphql.execution.defer +package graphql.execution.incremental import graphql.ExecutionResultImpl import graphql.GraphQLError import graphql.execution.NonNullableFieldWasNullException import graphql.execution.ResultPath +import graphql.execution.incremental.DeferredCall +import graphql.execution.incremental.DeferredCallContext import spock.lang.Specification import java.util.concurrent.CompletableFuture diff --git a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy b/src/test/groovy/graphql/execution/incremental/IncrementalContextDeferTest.groovy similarity index 96% rename from src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy rename to src/test/groovy/graphql/execution/incremental/IncrementalContextDeferTest.groovy index c0ff24abed..b4e53e716f 100644 --- a/src/test/groovy/graphql/execution/defer/IncrementalContextDeferTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/IncrementalContextDeferTest.groovy @@ -1,8 +1,11 @@ -package graphql.execution.defer +package graphql.execution.incremental import graphql.ExecutionResultImpl import graphql.execution.ResultPath +import graphql.execution.incremental.DeferredCall +import graphql.execution.incremental.DeferredCallContext +import graphql.execution.incremental.IncrementalContext import graphql.incremental.DelayedIncrementalExecutionResult import org.awaitility.Awaitility import spock.lang.Specification @@ -127,14 +130,14 @@ class IncrementalContextDeferTest extends Specification { def incrementalContext = new IncrementalContext() when: - def deferPresent1 = incrementalContext.isDeferDetected() + def deferPresent1 = incrementalContext.getIncrementalCallsDetected() then: !deferPresent1 when: incrementalContext.enqueue(offThread("A", 100, "/field/path")) - def deferPresent2 = incrementalContext.isDeferDetected() + def deferPresent2 = incrementalContext.getIncrementalCallsDetected() then: deferPresent2 diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index d2a979147e..105b455beb 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -34,7 +34,11 @@ class DataLoaderPerformanceTest extends Specification { def "760 ensure data loader is performant for lists"() { when: - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(query) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() def result = graphQL.execute(executionInput) then: @@ -43,13 +47,20 @@ class DataLoaderPerformanceTest extends Specification { // eg 1 for shops-->departments and one for departments --> products batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 1 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 1 + + where: + incrementalSupport << [true, false] } def "970 ensure data loader is performant for multiple field with lists"() { when: - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveQuery).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(expensiveQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() def result = graphQL.execute(executionInput) then: @@ -57,6 +68,9 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() <= 2 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() <= 2 + + where: + incrementalSupport << [true, false] } def "ensure data loader is performant for lists using async batch loading"() { @@ -65,7 +79,12 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers.useAsyncBatchLoading(true) - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(query) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() + def result = graphQL.execute(executionInput) then: @@ -75,6 +94,8 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 1 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 1 + where: + incrementalSupport << [true, false] } def "970 ensure data loader is performant for multiple field with lists using async batch loading"() { @@ -83,7 +104,12 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers.useAsyncBatchLoading(true) - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveQuery).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(expensiveQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() + def result = graphQL.execute(executionInput) then: @@ -91,6 +117,9 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() <= 2 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() <= 2 + + where: + incrementalSupport << [true, false] } def "data loader will work with deferred queries"() { diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 762018b0d5..130f45440f 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -40,7 +40,12 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification def "chainedInstrumentation: 760 ensure data loader is performant for lists"() { when: - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(query) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() + def result = graphQL.execute(executionInput) then: @@ -49,6 +54,9 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification // eg 1 for shops-->departments and one for departments --> products batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 1 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 1 + + where: + incrementalSupport << [true, false] } @Ignore("This test flakes on Travis for some reason. Clearly this indicates some sort of problem to investigate. However it also stop releases.") @@ -56,7 +64,11 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification when: - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveQuery).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(expensiveQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() def result = graphQL.execute(executionInput) @@ -66,6 +78,8 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 1 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 1 + where: + incrementalSupport << [true, false] } def "chainedInstrumentation: ensure data loader is performant for lists using async batch loading"() { @@ -74,7 +88,11 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification batchCompareDataFetchers.useAsyncBatchLoading(true) - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(query) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() def result = graphQL.execute(executionInput) then: @@ -84,6 +102,8 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() == 1 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 1 + where: + incrementalSupport << [true, false] } def "chainedInstrumentation: 970 ensure data loader is performant for multiple field with lists using async batch loading"() { @@ -92,7 +112,11 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification batchCompareDataFetchers.useAsyncBatchLoading(true) - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(expensiveQuery).dataLoaderRegistry(dataLoaderRegistry).build() + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(expensiveQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .incrementalSupport(incrementalSupport) + .build() def result = graphQL.execute(executionInput) then: @@ -100,6 +124,9 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification batchCompareDataFetchers.departmentsForShopsBatchLoaderCounter.get() <= 2 batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() <= 2 + + where: + incrementalSupport << [true, false] } def "chainedInstrumentation: data loader will work with deferred queries"() { From 934f88f6b8e466683d8ba1e74188077b40eaea4e Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 2 Feb 2024 18:42:11 +1100 Subject: [PATCH 124/393] Remove code duplication --- .../graphql/execution/ExecutionStrategy.java | 4 +- .../ExecutableNormalizedOperationFactory.java | 7 ++- .../incremental/IncrementalNodes.java | 52 ------------------- 3 files changed, 6 insertions(+), 57 deletions(-) delete mode 100644 src/main/java/graphql/normalized/incremental/IncrementalNodes.java diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5a29505f96..aa6f52c77c 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -357,7 +357,9 @@ private void addExtensionsIfPresent(ExecutionContext executionContext, DataFetch protected CompletableFuture handleFetchingException( DataFetchingEnvironment environment, - ExecutionStrategyParameters parameters, Throwable e) { + ExecutionStrategyParameters parameters, + Throwable e + ) { DataFetcherExceptionHandlerParameters handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() .dataFetchingEnvironment(environment) .exception(e) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 861775e3e7..03e45eb5a1 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -17,6 +17,7 @@ import graphql.execution.conditional.ConditionalNodes; import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; +import graphql.execution.incremental.IncrementalUtils; import graphql.introspection.Introspection; import graphql.language.Directive; import graphql.language.Document; @@ -30,7 +31,6 @@ import graphql.language.SelectionSet; import graphql.language.VariableDefinition; import graphql.normalized.incremental.NormalizedDeferExecution; -import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -190,7 +190,6 @@ public boolean getDeferSupport() { } private static final ConditionalNodes conditionalNodes = new ConditionalNodes(); - private static final IncrementalNodes incrementalNodes = new IncrementalNodes(); private ExecutableNormalizedOperationFactory() { @@ -769,10 +768,10 @@ private NormalizedDeferExecution buildDeferExecution( return null; } - return incrementalNodes.createDeferExecution( + return IncrementalUtils.createDeferExecution( this.coercedVariableValues.toMap(), directives, - newPossibleObjects + (label) -> new NormalizedDeferExecution(label, newPossibleObjects) ); } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java deleted file mode 100644 index 79574658a2..0000000000 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ /dev/null @@ -1,52 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.Assert; -import graphql.GraphQLContext; -import graphql.Internal; -import graphql.execution.CoercedVariables; -import graphql.execution.ValuesResolver; -import graphql.language.Directive; -import graphql.language.NodeUtil; -import graphql.schema.GraphQLObjectType; - -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import static graphql.Directives.DeferDirective; - -@Internal -public class IncrementalNodes { - - public NormalizedDeferExecution createDeferExecution( - Map variables, - List directives, - Set possibleTypes - ) { - Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); - - if (deferDirective != null) { - Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); - - Object flag = argumentValues.get("if"); - Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); - - if (!((Boolean) flag)) { - return null; - } - - Object label = argumentValues.get("label"); - - if (label == null) { - return new NormalizedDeferExecution(null, possibleTypes); - } - - Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - - return new NormalizedDeferExecution((String) label, possibleTypes); - } - - return null; - } -} From 52de911aea2608c3d9bb49b94caa7db6d1700bf9 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 2 Feb 2024 19:08:33 +1100 Subject: [PATCH 125/393] Various small adjustments in preparation to send the PR for review --- .../execution/incremental/DeferExecution.java | 6 + .../incremental/IncrementalUtils.java | 1 - .../ChainedInstrumentation.java | 2 + ...ecutionStrategyInstrumentationContext.java | 2 + .../instrumentation/Instrumentation.java | 2 + .../DataLoaderDispatcherInstrumentation.java | 13 +- .../FieldLevelTrackingApproach.java | 1 + ...nstrumentationDeferredFieldParameters.java | 6 +- .../groovy/example/http/DeferHttpSupport.java | 120 ------------------ src/test/groovy/example/http/HttpMain.java | 4 - .../LegacyTestingInstrumentation.groovy | 7 - .../groovy/readme/IncrementalExamples.java | 14 +- 12 files changed, 25 insertions(+), 153 deletions(-) delete mode 100644 src/test/groovy/example/http/DeferHttpSupport.java diff --git a/src/main/java/graphql/execution/incremental/DeferExecution.java b/src/main/java/graphql/execution/incremental/DeferExecution.java index e1aa0c1d2f..fc2c326426 100644 --- a/src/main/java/graphql/execution/incremental/DeferExecution.java +++ b/src/main/java/graphql/execution/incremental/DeferExecution.java @@ -4,6 +4,12 @@ import javax.annotation.Nullable; +/** + * Represents details about the defer execution that can be associated with a {@link graphql.execution.MergedField}. + *

+ * This representation is used during graphql execution. Check {@link graphql.normalized.incremental.NormalizedDeferExecution} + * for the normalized representation of @defer. + */ @ExperimentalApi public class DeferExecution { private final String label; diff --git a/src/main/java/graphql/execution/incremental/IncrementalUtils.java b/src/main/java/graphql/execution/incremental/IncrementalUtils.java index 521e8fdf15..0781777f35 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalUtils.java +++ b/src/main/java/graphql/execution/incremental/IncrementalUtils.java @@ -19,7 +19,6 @@ public class IncrementalUtils { private IncrementalUtils() {} - // TODO: Refactor this to reduce duplication with IncrementalNodes public static T createDeferExecution( Map variables, List directives, diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 886573b96f..61aa2df85f 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -5,6 +5,7 @@ import graphql.Assert; import graphql.ExecutionInput; import graphql.ExecutionResult; +import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.Async; import graphql.execution.ExecutionContext; @@ -178,6 +179,7 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument return new ChainedExecutionStrategyInstrumentationContext(mapAndDropNulls(instrumentations, mapper)); } + @ExperimentalApi @Override public InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState instrumentationState) { return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() diff --git a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java index 00228c6190..6b418fb073 100644 --- a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java @@ -1,6 +1,7 @@ package graphql.execution.instrumentation; import graphql.ExecutionResult; +import graphql.ExperimentalApi; import graphql.Internal; import graphql.PublicSpi; import graphql.execution.FieldValueInfo; @@ -21,6 +22,7 @@ default void onFieldValuesException() { } + @ExperimentalApi default void onDeferredField(MergedField field) { } diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 9445530922..8143369b22 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -2,6 +2,7 @@ import graphql.ExecutionInput; import graphql.ExecutionResult; +import graphql.ExperimentalApi; import graphql.PublicSpi; import graphql.execution.ExecutionContext; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; @@ -229,6 +230,7 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} * @return a non-null {@link InstrumentationContext} object that will be called back when the step ends */ + @ExperimentalApi default InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { return noOp(); } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 8ab5fd7dfa..47ced2ef90 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -24,8 +24,6 @@ import org.dataloader.stats.Statistics; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.LinkedHashMap; import java.util.Map; @@ -139,16 +137,7 @@ public InstrumentationContext beginDeferredField(Instrumentatio // if there are no data loaders, there is nothing to do // if (state.hasNoDataLoaders()) { - return new InstrumentationContext<>() { - @Override - public void onDispatched(CompletableFuture result) { - } - - @Override - public void onCompleted(ExecutionResult result, Throwable t) { - } - }; - + return ExecutionStrategyInstrumentationContext.NOOP; } return state.getApproach().beginDeferredField(parameters, state.getState()); } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index e9a1947e90..cc6537158c 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -183,6 +183,7 @@ private int getCountForList(List fieldValueInfos) { } InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { + // Create a new CallStack, since every deferred fields can execute in parallel. CallStack callStack = new CallStack(); int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); synchronized (callStack) { diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java index df67d63313..dd51d088d4 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java @@ -1,15 +1,13 @@ package graphql.execution.instrumentation.parameters; +import graphql.ExperimentalApi; import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; import graphql.execution.ExecutionStrategyParameters; -import graphql.execution.instrumentation.InstrumentationState; - -import java.util.function.Supplier; /** * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods */ +@ExperimentalApi public class InstrumentationDeferredFieldParameters { private final ExecutionContext executionContext; private final ExecutionStrategyParameters executionStrategyParameters; diff --git a/src/test/groovy/example/http/DeferHttpSupport.java b/src/test/groovy/example/http/DeferHttpSupport.java deleted file mode 100644 index 3f3648449f..0000000000 --- a/src/test/groovy/example/http/DeferHttpSupport.java +++ /dev/null @@ -1,120 +0,0 @@ -package example.http; - -import graphql.incremental.DelayedIncrementalExecutionResult; -import graphql.incremental.IncrementalExecutionResult; -import jakarta.servlet.http.HttpServletResponse; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.UncheckedIOException; -import java.util.Map; - -public class DeferHttpSupport { - - private static final String CRLF = "\r\n"; - - @SuppressWarnings("unchecked") - static void sendIncrementalResponse(HttpServletResponse response, IncrementalExecutionResult incrementalExecutionResult) { - - Publisher incrementalResults = incrementalExecutionResult.getIncrementalItemPublisher(); - - try { - sendMultipartResponse(response, incrementalExecutionResult, incrementalResults); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - static private void sendMultipartResponse( - HttpServletResponse response, - IncrementalExecutionResult incrementalExecutionResult, - Publisher incrementalResults - ) { - // this implements this apollo defer spec: https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md - // the spec says CRLF + "-----" + CRLF is needed at the end, but it works without it and with it we get client - // side errors with it, so we skp it - response.setStatus(HttpServletResponse.SC_OK); - - response.setHeader("Content-Type", "multipart/mixed; boundary=\"-\""); - response.setHeader("Connection", "keep-alive"); - - // send the first "un deferred" part of the result - writeAndFlushPart(response, incrementalExecutionResult.toSpecification()); - - // now send each deferred part which is given to us as a reactive stream - // of deferred values - incrementalResults.subscribe(new Subscriber() { - Subscription subscription; - - @Override - public void onSubscribe(Subscription s) { - this.subscription = s; - s.request(1); - } - - @Override - public void onNext(DelayedIncrementalExecutionResult delayedIncrementalExecutionResult) { - subscription.request(1); - - writeAndFlushPart(response, delayedIncrementalExecutionResult.toSpecification()); - } - - @Override - public void onError(Throwable t) { - t.printStackTrace(System.err); - } - - @Override - public void onComplete() { - } - }); - - } - - private static void writeAndFlushPart(HttpServletResponse response, Map result) { - DeferMultiPart deferMultiPart = new DeferMultiPart(result); - StringBuilder sb = new StringBuilder(); - sb.append(CRLF).append("---").append(CRLF); - String body = deferMultiPart.write(); - sb.append(body); - writeAndFlush(response, sb); - } - - private static void writeAndFlush(HttpServletResponse response, StringBuilder sb) { - try { - PrintWriter writer = response.getWriter(); - writer.write(sb.toString()); - writer.flush(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - - private static class DeferMultiPart { - - private Object body; - - public DeferMultiPart(Object data) { - this.body = data; - } - - public String write() { - StringBuilder result = new StringBuilder(); - String bodyString = bodyToString(); - result.append("Content-Type: application/json").append(CRLF); - result.append("Content-Length: ").append(bodyString.length()).append(CRLF).append(CRLF); - result.append(bodyString); - return result.toString(); - } - - private String bodyToString() { - return JsonKit.GSON.toJson(body); - } - } - -} diff --git a/src/test/groovy/example/http/HttpMain.java b/src/test/groovy/example/http/HttpMain.java index 86507f33fa..1e47c739a8 100644 --- a/src/test/groovy/example/http/HttpMain.java +++ b/src/test/groovy/example/http/HttpMain.java @@ -8,7 +8,6 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; import graphql.execution.instrumentation.tracing.TracingInstrumentation; -import graphql.incremental.IncrementalExecutionResult; import graphql.schema.DataFetcher; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -161,9 +160,6 @@ private void handleStarWars(HttpServletRequest httpRequest, HttpServletResponse private void returnAsJson(HttpServletResponse response, ExecutionResult executionResult) throws IOException { - if (executionResult instanceof IncrementalExecutionResult) { - DeferHttpSupport.sendIncrementalResponse(response, (IncrementalExecutionResult) executionResult); - } sendNormalResponse(response, executionResult); } diff --git a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy index 8bd57196a1..c696af74cd 100644 --- a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy @@ -68,13 +68,6 @@ class LegacyTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("execute-operation", executionList, throwableList, useOnDispatch) } - // TODO: Need that? -// @Override -// DeferredFieldInstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters) { -// assert parameters.getInstrumentationState() == instrumentationState -// return new TestingInstrumentContext("deferred-field-$parameters.field.name", executionList, throwableList) -// } - @Override InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage diff --git a/src/test/groovy/readme/IncrementalExamples.java b/src/test/groovy/readme/IncrementalExamples.java index e53e8cc1eb..0a5f820acd 100644 --- a/src/test/groovy/readme/IncrementalExamples.java +++ b/src/test/groovy/readme/IncrementalExamples.java @@ -12,8 +12,6 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import java.util.Map; - @SuppressWarnings({"unused", "ConstantConditions"}) public class IncrementalExamples { @@ -35,15 +33,21 @@ void basicExample(HttpServletResponse httpServletResponse, String deferredQuery) // ExecutionResult initialResult = graphQL.execute(ExecutionInput.newExecutionInput().query(deferredQuery).build()); + if (!(initialResult instanceof IncrementalExecutionResult)) { + // handle non incremental response + return; + } + + IncrementalExecutionResult incrementalResult = (IncrementalExecutionResult) initialResult; + // // then initial results happen first, the incremental ones will begin AFTER these initial // results have completed // sendMultipartHttpResult(httpServletResponse, initialResult); - Map extensions = initialResult.getExtensions(); - Publisher delayedIncrementalResults = - ((IncrementalExecutionResult) initialResult).getIncrementalItemPublisher(); + Publisher delayedIncrementalResults = incrementalResult + .getIncrementalItemPublisher(); // // you subscribe to the incremental results like any other reactive stream From f1e03a69a88ff558c0471cd2928bd4872352f14f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 2 Feb 2024 19:10:12 +1100 Subject: [PATCH 126/393] Remove unused import --- .../instrumentation/LegacyTestingInstrumentation.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy index c696af74cd..e8a9478cb6 100644 --- a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy @@ -3,7 +3,6 @@ package graphql.execution.instrumentation import graphql.ExecutionInput import graphql.ExecutionResult import graphql.execution.ExecutionContext -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters From 635f5ac9b90ce9162e92c5651d8740269b3e86d8 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 4 Feb 2024 20:23:51 +1000 Subject: [PATCH 127/393] adding a tracking agent --- agent-test/build.gradle | 40 ++++++++++++ .../java/graphql/test/agent/AgentTest.java | 16 +++++ .../java/graphql/test/agent/TestQuery.java | 44 +++++++++++++ agent/build.gradle | 38 +++++++++++ .../java/graphql/agent/GraphQLJavaAgent.java | 65 +++++++++++++++++++ settings.gradle | 1 + .../agent/result/ExecutionTrackingResult.java | 26 ++++++++ 7 files changed, 230 insertions(+) create mode 100644 agent-test/build.gradle create mode 100644 agent-test/src/main/java/graphql/test/agent/AgentTest.java create mode 100644 agent-test/src/main/java/graphql/test/agent/TestQuery.java create mode 100644 agent/build.gradle create mode 100644 agent/src/main/java/graphql/agent/GraphQLJavaAgent.java create mode 100644 src/main/java/graphql/agent/result/ExecutionTrackingResult.java diff --git a/agent-test/build.gradle b/agent-test/build.gradle new file mode 100644 index 0000000000..ad0cedab82 --- /dev/null +++ b/agent-test/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java' +} + +dependencies { + implementation(rootProject) + implementation("net.bytebuddy:byte-buddy-agent:1.14.11") +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + + +repositories { + mavenCentral() + mavenLocal() +} + + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + + +jar { + manifest { + attributes( + 'Agent-Class': 'graphql.agent.GraphQLJavaAgent', + 'Can-Redefine-Classes': 'true', + 'Can-Retransform-Classes': 'true', + 'Premain-Class': 'graphql.agent.GraphQLJavaAgent' + ) + } +} + diff --git a/agent-test/src/main/java/graphql/test/agent/AgentTest.java b/agent-test/src/main/java/graphql/test/agent/AgentTest.java new file mode 100644 index 0000000000..c3e3c94a40 --- /dev/null +++ b/agent-test/src/main/java/graphql/test/agent/AgentTest.java @@ -0,0 +1,16 @@ +package graphql.test.agent; + +import net.bytebuddy.agent.ByteBuddyAgent; + +import java.io.File; + + +public class AgentTest { + + + public static void main(String[] args) { + ByteBuddyAgent.attach(new File("agent/build/libs/agent.jar"), String.valueOf(ProcessHandle.current().pid())); + TestQuery.executeQuery(); + } + +} diff --git a/agent-test/src/main/java/graphql/test/agent/TestQuery.java b/agent-test/src/main/java/graphql/test/agent/TestQuery.java new file mode 100644 index 0000000000..7a33d65ec1 --- /dev/null +++ b/agent-test/src/main/java/graphql/test/agent/TestQuery.java @@ -0,0 +1,44 @@ +package graphql.test.agent; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.agent.result.ExecutionTrackingResult; +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLSchema; +import graphql.schema.PropertyDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; + +import java.util.List; +import java.util.Map; + +public class TestQuery { + + static DataFetcher issuesDF = (env) -> { + return List.of( + Map.of("id", "1", "title", "issue-1"), + Map.of("id", "2", "title", "issue-2")); + }; + + static void executeQuery() { + String sdl = "type Query{issues: [Issue]} type Issue {id: ID, title: String}"; + TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(sdl); + PropertyDataFetcher propertyDataFetcher = new PropertyDataFetcher("test"); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("issues", issuesDF)) + .build(); + GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("{issues{id title}}").build(); + ExecutionResult result = graphQL.execute(executionInput); + System.out.println(result); + ExecutionTrackingResult trackingResult = executionInput.getGraphQLContext().get(ExecutionTrackingResult.EXECUTION_TRACKING_KEY); + System.out.println(trackingResult); + } +} diff --git a/agent/build.gradle b/agent/build.gradle new file mode 100644 index 0000000000..15b46645b9 --- /dev/null +++ b/agent/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'java' + id "com.github.johnrengelman.shadow" version "8.1.1" +} + +dependencies { + implementation("net.bytebuddy:byte-buddy:1.14.11") + implementation(rootProject) +} + +repositories { + mavenCentral() + mavenLocal() +} + + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +shadowJar { + minimize() + archiveClassifier.set('') + configurations = [project.configurations.compileClasspath] + dependencies { + exclude(dependency(rootProject)) + } + manifest { + attributes( + 'Agent-Class': 'graphql.agent.GraphQLJavaAgent', + 'Can-Redefine-Classes': 'true', + 'Can-Retransform-Classes': 'true', + 'Premain-Class': 'graphql.agent.GraphQLJavaAgent' + ) + } +} diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java new file mode 100644 index 0000000000..0e79aa22c2 --- /dev/null +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -0,0 +1,65 @@ +package graphql.agent; + +import graphql.agent.result.ExecutionTrackingResult; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStrategyParameters; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; + +import java.lang.instrument.Instrumentation; + +import static graphql.agent.result.ExecutionTrackingResult.EXECUTION_TRACKING_KEY; +import static net.bytebuddy.matcher.ElementMatchers.nameMatches; +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class GraphQLJavaAgent { + + public static void agentmain(String agentArgs, Instrumentation inst) { + System.out.println("Agent is running"); + new AgentBuilder.Default() +// .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) +// .with(AgentBuilder.LambdaInstrumentationStrategy.ENABLED) + .disableClassFormatChanges() + .type(named("graphql.execution.ExecutionStrategy")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + System.out.println("Transforming " + typeDescription); + return builder + .visit(Advice.to(DataFetcherInvokeAdvice.class).on(nameMatches("invokeDataFetcher"))); + }) + .installOn(inst); + + new AgentBuilder.Default() + .disableClassFormatChanges() + .type(named("graphql.execution.Execution")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); + }).installOn(inst); + } +} + +class ExecutionAdvice { + + @Advice.OnMethodEnter + public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { + executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); + } +} + +class DataFetcherInvokeAdvice { + @Advice.OnMethodEnter + public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext executionContext, + @Advice.Argument(1) ExecutionStrategyParameters parameters) { + ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); + executionTrackingResult.start(parameters.getPath(), System.nanoTime()); + } + + @Advice.OnMethodExit + public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext executionContext, + @Advice.Argument(1) ExecutionStrategyParameters parameters) { + ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); + executionTrackingResult.end(parameters.getPath(), System.nanoTime()); + } + +} diff --git a/settings.gradle b/settings.gradle index 160b054c14..e337acd34b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,3 +14,4 @@ pluginManagement { } rootProject.name = 'graphql-java' +include("agent", "agent-test") diff --git a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java new file mode 100644 index 0000000000..cbecf8f3c8 --- /dev/null +++ b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java @@ -0,0 +1,26 @@ +package graphql.agent.result; + +import graphql.execution.ResultPath; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class ExecutionTrackingResult { + public static final String EXECUTION_TRACKING_KEY = "__GJ_AGENT_EXECUTION_TRACKING"; + private final Map timePerPath = new LinkedHashMap<>(); + + public void start(ResultPath path, long startTime) { + timePerPath.put(path, startTime); + } + + public void end(ResultPath path, long endTime) { + timePerPath.put(path, endTime - timePerPath.get(path)); + } + + @Override + public String toString() { + return "ExecutionTrackingResult{" + + "timePerPath=" + timePerPath + + '}'; + } +} From 384cc3735ad79a9b7c37fbefcfcfdf9fd743e6d5 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 5 Feb 2024 05:45:17 +1000 Subject: [PATCH 128/393] adding tests --- agent-test/build.gradle | 6 +++ .../src/test/java/graphql/test/AgentTest.java | 27 +++++++++++ .../src/test/java/graphql/test/LoadAgent.java | 15 +++++++ .../src/test/java/graphql/test/TestQuery.java | 45 +++++++++++++++++++ .../agent/result/ExecutionTrackingResult.java | 12 +++++ 5 files changed, 105 insertions(+) create mode 100644 agent-test/src/test/java/graphql/test/AgentTest.java create mode 100644 agent-test/src/test/java/graphql/test/LoadAgent.java create mode 100644 agent-test/src/test/java/graphql/test/TestQuery.java diff --git a/agent-test/build.gradle b/agent-test/build.gradle index ad0cedab82..c3fb69255c 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -5,6 +5,12 @@ plugins { dependencies { implementation(rootProject) implementation("net.bytebuddy:byte-buddy-agent:1.14.11") + + testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation("org.assertj:assertj-core:3.25.1") + } java { diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java new file mode 100644 index 0000000000..a7b9bd34f0 --- /dev/null +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -0,0 +1,27 @@ +package graphql.test; + +import graphql.agent.result.ExecutionTrackingResult; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AgentTest { + + @BeforeAll + static void init() { + LoadAgent.load(); + } + + @AfterAll + static void cleanup() { + } + + @Test + void test() { + ExecutionTrackingResult executionTrackingResult = TestQuery.executeQuery(); + assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(4); + assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); + } +} diff --git a/agent-test/src/test/java/graphql/test/LoadAgent.java b/agent-test/src/test/java/graphql/test/LoadAgent.java new file mode 100644 index 0000000000..b549de5239 --- /dev/null +++ b/agent-test/src/test/java/graphql/test/LoadAgent.java @@ -0,0 +1,15 @@ +package graphql.test; + +import net.bytebuddy.agent.ByteBuddyAgent; + +import java.io.File; + + +public class LoadAgent { + + + public static void load() { + ByteBuddyAgent.attach(new File("../agent/build/libs/agent.jar"), String.valueOf(ProcessHandle.current().pid())); + } + +} diff --git a/agent-test/src/test/java/graphql/test/TestQuery.java b/agent-test/src/test/java/graphql/test/TestQuery.java new file mode 100644 index 0000000000..b0ee524a69 --- /dev/null +++ b/agent-test/src/test/java/graphql/test/TestQuery.java @@ -0,0 +1,45 @@ +package graphql.test; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.agent.result.ExecutionTrackingResult; +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLSchema; +import graphql.schema.PropertyDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import org.assertj.core.api.Assertions; + +import java.util.List; +import java.util.Map; + +public class TestQuery { + + static DataFetcher issuesDF = (env) -> { + return List.of( + Map.of("id", "1", "title", "issue-1"), + Map.of("id", "2", "title", "issue-2")); + }; + + static ExecutionTrackingResult executeQuery() { + String sdl = "type Query{issues: [Issue]} type Issue {id: ID, title: String}"; + TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(sdl); + PropertyDataFetcher propertyDataFetcher = new PropertyDataFetcher("test"); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("issues", issuesDF)) + .build(); + GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("{issues{id title}}").build(); + ExecutionResult result = graphQL.execute(executionInput); + Assertions.assertThat(result.getErrors()).isEmpty(); + ExecutionTrackingResult trackingResult = executionInput.getGraphQLContext().get(ExecutionTrackingResult.EXECUTION_TRACKING_KEY); + return trackingResult; + } +} diff --git a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java index cbecf8f3c8..a023bee65e 100644 --- a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java +++ b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java @@ -17,6 +17,18 @@ public void end(ResultPath path, long endTime) { timePerPath.put(path, endTime - timePerPath.get(path)); } + public int dataFetcherCount() { + return timePerPath.size(); + } + + public long getTime(ResultPath path) { + return timePerPath.get(path); + } + + public long getTime(String path) { + return timePerPath.get(ResultPath.parse(path)); + } + @Override public String toString() { return "ExecutionTrackingResult{" + From 224038566dbf8786d9f7fad953fd6311e7eaa45f Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 5 Feb 2024 05:50:58 +1000 Subject: [PATCH 129/393] test setup --- agent-test/build.gradle | 10 ++++++++++ agent-test/src/test/java/graphql/test/AgentTest.java | 2 +- agent/build.gradle | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/agent-test/build.gradle b/agent-test/build.gradle index c3fb69255c..745b49f577 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -19,6 +19,16 @@ java { } } +tasks.named('test', Test) { + useJUnitPlatform() + + maxHeapSize = '4G' + + testLogging { + events "passed" + } +} + repositories { mavenCentral() diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java index a7b9bd34f0..0adfbb31ef 100644 --- a/agent-test/src/test/java/graphql/test/AgentTest.java +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -21,7 +21,7 @@ static void cleanup() { @Test void test() { ExecutionTrackingResult executionTrackingResult = TestQuery.executeQuery(); - assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(4); + assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(5); assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); } } diff --git a/agent/build.gradle b/agent/build.gradle index 15b46645b9..6484b1e73d 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -19,6 +19,10 @@ java { languageVersion = JavaLanguageVersion.of(11) } } +tasks.named('test', Test) { + dependsOn('shadowJar') +} + shadowJar { minimize() From fe95c7f75944a0f51d60fc17362d7ba6f20e7219 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 5 Feb 2024 05:52:46 +1000 Subject: [PATCH 130/393] test setup --- agent/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent/build.gradle b/agent/build.gradle index 6484b1e73d..32742db3cc 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -19,6 +19,9 @@ java { languageVersion = JavaLanguageVersion.of(11) } } + +// making sure the fat jar is build when running tests +// so that the agent-test module can load it during tests tasks.named('test', Test) { dependsOn('shadowJar') } From 91156f58cdb156d48214ba29b23c77676ecfdb7f Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 5 Feb 2024 06:03:14 +1000 Subject: [PATCH 131/393] tests --- agent-test/build.gradle | 1 + .../src/test/java/graphql/test/AgentTest.java | 2 ++ agent/build.gradle | 7 ------ .../java/graphql/agent/GraphQLJavaAgent.java | 19 ++++++++++++-- .../agent/result/ExecutionTrackingResult.java | 25 +++++++++++++++++-- 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/agent-test/build.gradle b/agent-test/build.gradle index 745b49f577..f49f603f71 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -20,6 +20,7 @@ java { } tasks.named('test', Test) { + dependsOn(':agent:shadowJar') useJUnitPlatform() maxHeapSize = '4G' diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java index 0adfbb31ef..cdf0fc1ff7 100644 --- a/agent-test/src/test/java/graphql/test/AgentTest.java +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -23,5 +23,7 @@ void test() { ExecutionTrackingResult executionTrackingResult = TestQuery.executeQuery(); assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(5); assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); + assertThat(executionTrackingResult.getDfResultTypes("/issues")) + .isEqualTo(ExecutionTrackingResult.DFResultType.DONE_OK); } } diff --git a/agent/build.gradle b/agent/build.gradle index 32742db3cc..15b46645b9 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -20,13 +20,6 @@ java { } } -// making sure the fat jar is build when running tests -// so that the agent-test module can load it during tests -tasks.named('test', Test) { - dependsOn('shadowJar') -} - - shadowJar { minimize() archiveClassifier.set('') diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 0e79aa22c2..e41b0dcc1d 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -3,10 +3,12 @@ import graphql.agent.result.ExecutionTrackingResult; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.ResultPath; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import java.lang.instrument.Instrumentation; +import java.util.concurrent.CompletableFuture; import static graphql.agent.result.ExecutionTrackingResult.EXECUTION_TRACKING_KEY; import static net.bytebuddy.matcher.ElementMatchers.nameMatches; @@ -57,9 +59,22 @@ public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext e @Advice.OnMethodExit public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext executionContext, - @Advice.Argument(1) ExecutionStrategyParameters parameters) { + @Advice.Argument(1) ExecutionStrategyParameters parameters, + @Advice.Return CompletableFuture result) { ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); - executionTrackingResult.end(parameters.getPath(), System.nanoTime()); + ResultPath path = parameters.getPath(); + executionTrackingResult.end(path, System.nanoTime()); + if (result.isDone()) { + if (result.isCancelled()) { + executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.DONE_CANCELLED); + } else if (result.isCompletedExceptionally()) { + executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.DONE_EXCEPTIONALLY); + } else { + executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.DONE_OK); + } + } else { + executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.PENDING); + } } } diff --git a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java index a023bee65e..bcc297e5e0 100644 --- a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java +++ b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java @@ -2,12 +2,21 @@ import graphql.execution.ResultPath; -import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class ExecutionTrackingResult { + + public enum DFResultType { + DONE_OK, + DONE_EXCEPTIONALLY, + DONE_CANCELLED, + PENDING, + } + public static final String EXECUTION_TRACKING_KEY = "__GJ_AGENT_EXECUTION_TRACKING"; - private final Map timePerPath = new LinkedHashMap<>(); + private final Map timePerPath = new ConcurrentHashMap<>(); + private final Map dfResultTypes = new ConcurrentHashMap<>(); public void start(ResultPath path, long startTime) { timePerPath.put(path, startTime); @@ -29,6 +38,18 @@ public long getTime(String path) { return timePerPath.get(ResultPath.parse(path)); } + public void setDfResultTypes(ResultPath resultPath, DFResultType resultTypes) { + dfResultTypes.put(resultPath, resultTypes); + } + + public DFResultType getDfResultTypes(ResultPath resultPath) { + return dfResultTypes.get(resultPath); + } + + public DFResultType getDfResultTypes(String resultPath) { + return dfResultTypes.get(ResultPath.parse(resultPath)); + } + @Override public String toString() { return "ExecutionTrackingResult{" + From ef671d077e2e82df5f33213699d41a9c8acc2819 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 5 Feb 2024 11:40:41 +1100 Subject: [PATCH 132/393] PR feedback --- .../execution/AsyncExecutionStrategy.java | 48 +++++----- .../java/graphql/execution/Execution.java | 8 +- .../graphql/execution/ExecutionContext.java | 8 +- .../graphql/execution/FieldCollector.java | 24 ++--- .../java/graphql/execution/MergedField.java | 30 +++---- ...rExecution.java => DeferredExecution.java} | 4 +- ...Context.java => IncrementalCallState.java} | 14 +-- .../incremental/IncrementalUtils.java | 2 +- .../ExecutableNormalizedOperationFactory.java | 2 +- ...y => IncrementalCallStateDeferTest.groovy} | 89 +++++++++---------- 10 files changed, 113 insertions(+), 116 deletions(-) rename src/main/java/graphql/execution/incremental/{DeferExecution.java => DeferredExecution.java} (88%) rename src/main/java/graphql/execution/incremental/{IncrementalContext.java => IncrementalCallState.java} (93%) rename src/test/groovy/graphql/execution/incremental/{IncrementalContextDeferTest.groovy => IncrementalCallStateDeferTest.groovy} (68%) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 47f2673e54..03fe907bac 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -8,7 +8,7 @@ import graphql.execution.incremental.DeferredCall; import graphql.execution.incremental.DeferredCallContext; import graphql.execution.incremental.IncrementalCall; -import graphql.execution.incremental.DeferExecution; +import graphql.execution.incremental.DeferredExecution; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; @@ -64,19 +64,19 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); - DeferExecutionSupport deferExecutionSupport = executionContext.isIncrementalSupport() ? - new DeferExecutionSupport.DeferExecutionSupportImpl( + DeferredExecutionSupport deferredExecutionSupport = executionContext.isIncrementalSupport() ? + new DeferredExecutionSupport.DeferredExecutionSupportImpl( fields, parameters, executionContext, this::resolveFieldWithInfo - ) : DeferExecutionSupport.NOOP; + ) : DeferredExecutionSupport.NOOP; - executionContext.getIncrementalContext().enqueue(deferExecutionSupport.createCalls()); + executionContext.getIncrementalCallState().enqueue(deferredExecutionSupport.createCalls()); // Only non-deferred fields should be considered for calculating the expected size of futures. Async.CombinedBuilder futures = Async - .ofExpectedSize(fields.size() - deferExecutionSupport.deferredFieldsCount()); + .ofExpectedSize(fields.size() - deferredExecutionSupport.deferredFieldsCount()); for (String fieldName : fieldNames) { MergedField currentField = fields.getSubField(fieldName); @@ -87,7 +87,7 @@ public CompletableFuture execute(ExecutionContext executionCont CompletableFuture future; - if (deferExecutionSupport.isDeferredField(currentField)) { + if (deferredExecutionSupport.isDeferredField(currentField)) { executionStrategyCtx.onDeferredField(currentField); } else { future = resolveFieldWithInfo(executionContext, newParameters); @@ -98,7 +98,7 @@ public CompletableFuture execute(ExecutionContext executionCont executionStrategyCtx.onDispatched(overallResult); futures.await().whenComplete((completeValueInfos, throwable) -> { - List fieldsExecutedOnInitialResult = deferExecutionSupport.getNonDeferredFieldNames(fieldNames); + List fieldsExecutedOnInitialResult = deferredExecutionSupport.getNonDeferredFieldNames(fieldNames); BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldsExecutedOnInitialResult, overallResult); if (throwable != null) { @@ -132,9 +132,9 @@ public CompletableFuture execute(ExecutionContext executionCont * The {@link NoOp} instance should be used when incremental support is not enabled for the current execution. The * methods in this class will return empty or no-op results, that should not impact the main execution. *

- * {@link DeferExecutionSupportImpl} is the actual implementation that will be used when incremental support is enabled. + * {@link DeferredExecutionSupportImpl} is the actual implementation that will be used when incremental support is enabled. */ - private interface DeferExecutionSupport { + private interface DeferredExecutionSupport { boolean isDeferredField(MergedField mergedField); @@ -144,13 +144,13 @@ private interface DeferExecutionSupport { Set> createCalls(); - DeferExecutionSupport NOOP = new DeferExecutionSupport.NoOp(); + DeferredExecutionSupport NOOP = new DeferredExecutionSupport.NoOp(); /** * An implementation that actually executes the deferred fields. */ - class DeferExecutionSupportImpl implements DeferExecutionSupport { - private final ImmutableListMultimap deferExecutionToFields; + class DeferredExecutionSupportImpl implements DeferredExecutionSupport { + private final ImmutableListMultimap deferredExecutionToFields; private final ImmutableSet deferredFields; private final ImmutableList nonDeferredFieldNames; private final ExecutionStrategyParameters parameters; @@ -158,7 +158,7 @@ class DeferExecutionSupportImpl implements DeferExecutionSupport { private final BiFunction> resolveFieldWithInfoFn; private final Map>> dfCache = new HashMap<>(); - private DeferExecutionSupportImpl( + private DeferredExecutionSupportImpl( MergedSelectionSet mergedSelectionSet, ExecutionStrategyParameters parameters, ExecutionContext executionContext, @@ -166,22 +166,22 @@ private DeferExecutionSupportImpl( ) { this.executionContext = executionContext; this.resolveFieldWithInfoFn = resolveFieldWithInfoFn; - ImmutableListMultimap.Builder deferExecutionToFieldsBuilder = ImmutableListMultimap.builder(); + ImmutableListMultimap.Builder deferredExecutionToFieldsBuilder = ImmutableListMultimap.builder(); ImmutableSet.Builder deferredFieldsBuilder = ImmutableSet.builder(); ImmutableList.Builder nonDeferredFieldNamesBuilder = ImmutableList.builder(); mergedSelectionSet.getSubFields().values().forEach(mergedField -> { - mergedField.getDeferExecutions().forEach(de -> { - deferExecutionToFieldsBuilder.put(de, mergedField); + mergedField.getDeferredExecutions().forEach(de -> { + deferredExecutionToFieldsBuilder.put(de, mergedField); deferredFieldsBuilder.add(mergedField); }); - if (mergedField.getDeferExecutions().isEmpty()) { + if (mergedField.getDeferredExecutions().isEmpty()) { nonDeferredFieldNamesBuilder.add(mergedField.getSingleField().getResultKey()); } }); - this.deferExecutionToFields = deferExecutionToFieldsBuilder.build(); + this.deferredExecutionToFields = deferredExecutionToFieldsBuilder.build(); this.deferredFields = deferredFieldsBuilder.build(); this.parameters = parameters; this.nonDeferredFieldNames = nonDeferredFieldNamesBuilder.build(); @@ -203,22 +203,22 @@ public List getNonDeferredFieldNames(List allFieldNames) { } @Override public Set> createCalls() { - return deferExecutionToFields.keySet().stream() + return deferredExecutionToFields.keySet().stream() .map(this::createDeferredCall) .collect(Collectors.toSet()); } - private DeferredCall createDeferredCall(DeferExecution deferExecution) { + private DeferredCall createDeferredCall(DeferredExecution deferredExecution) { DeferredCallContext deferredCallContext = new DeferredCallContext(); - List mergedFields = deferExecutionToFields.get(deferExecution); + List mergedFields = deferredExecutionToFields.get(deferredExecution); List>> calls = mergedFields.stream() .map(currentField -> this.createResultSupplier(currentField, deferredCallContext)) .collect(Collectors.toList()); return new DeferredCall( - deferExecution.getLabel(), + deferredExecution.getLabel(), this.parameters.getPath(), calls, deferredCallContext @@ -280,7 +280,7 @@ private Supplier> creat /** * A no-op implementation that should be used when incremental support is not enabled for the current execution. */ - class NoOp implements DeferExecutionSupport { + class NoOp implements DeferredExecutionSupport { @Override public boolean isDeferredField(MergedField mergedField) { diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 55d7eecb2e..37955a7aaa 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -7,7 +7,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; -import graphql.execution.incremental.IncrementalContext; +import graphql.execution.incremental.IncrementalCallState; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -187,11 +187,11 @@ private CompletableFuture executeOperation(ExecutionContext exe */ private CompletableFuture deferSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { - IncrementalContext incrementalContext = executionContext.getIncrementalContext(); - if (incrementalContext.getIncrementalCallsDetected()) { + IncrementalCallState incrementalCallState = executionContext.getIncrementalCallState(); + if (incrementalCallState.getIncrementalCallsDetected()) { // we start the rest of the query now to maximize throughput. We have the initial important results, // and now we can start the rest of the calls as early as possible (even before someone subscribes) - Publisher publisher = incrementalContext.startDeferredCalls(); + Publisher publisher = incrementalCallState.startDeferredCalls(); return IncrementalExecutionResultImpl.fromExecutionResult(er) // "hasNext" can, in theory, be "false" when all the incremental items are delivered in the diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index d5bf2ad484..4c62e58c0b 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -8,7 +8,7 @@ import graphql.GraphQLError; import graphql.PublicApi; import graphql.collect.ImmutableKit; -import graphql.execution.incremental.IncrementalContext; +import graphql.execution.incremental.IncrementalCallState; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; @@ -54,7 +54,7 @@ public class ExecutionContext { private final Set errorPaths = new HashSet<>(); private final DataLoaderRegistry dataLoaderRegistry; private final Locale locale; - private final IncrementalContext incrementalContext = new IncrementalContext(); + private final IncrementalCallState incrementalCallState = new IncrementalCallState(); private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; @@ -257,8 +257,8 @@ public ExecutionStrategy getSubscriptionStrategy() { return subscriptionStrategy; } - public IncrementalContext getIncrementalContext() { - return incrementalContext; + public IncrementalCallState getIncrementalCallState() { + return incrementalCallState; } public boolean isIncrementalSupport() { diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index 6e5e411812..674819407d 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -3,7 +3,7 @@ import graphql.Internal; import graphql.execution.conditional.ConditionalNodes; -import graphql.execution.incremental.DeferExecution; +import graphql.execution.incremental.DeferredExecution; import graphql.execution.incremental.IncrementalUtils; import graphql.language.Field; import graphql.language.FragmentDefinition; @@ -70,11 +70,11 @@ public MergedSelectionSet collectFields(FieldCollectorParameters parameters, Sel } - private void collectFields(FieldCollectorParameters parameters, SelectionSet selectionSet, Set visitedFragments, Map fields, DeferExecution deferExecution, boolean incrementalSupport) { + private void collectFields(FieldCollectorParameters parameters, SelectionSet selectionSet, Set visitedFragments, Map fields, DeferredExecution deferredExecution, boolean incrementalSupport) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { - collectField(parameters, fields, (Field) selection, deferExecution); + collectField(parameters, fields, (Field) selection, deferredExecution); } else if (selection instanceof InlineFragment) { collectInlineFragment(parameters, visitedFragments, fields, (InlineFragment) selection, incrementalSupport); } else if (selection instanceof FragmentSpread) { @@ -106,13 +106,13 @@ private void collectFragmentSpread(FieldCollectorParameters parameters, Set visitedFragments, Map fields, InlineFragment inlineFragment, boolean incrementalSupport) { @@ -124,16 +124,16 @@ private void collectInlineFragment(FieldCollectorParameters parameters, Set fields, Field field, DeferExecution deferExecution) { + private void collectField(FieldCollectorParameters parameters, Map fields, Field field, DeferredExecution deferredExecution) { if (!conditionalNodes.shouldInclude(field, parameters.getVariables(), parameters.getGraphQLSchema(), @@ -145,12 +145,12 @@ private void collectField(FieldCollectorParameters parameters, Map builder .addField(field) - .addDeferExecution(deferExecution)) + .addDeferredExecution(deferredExecution)) ); } else { fields.put(name, MergedField .newMergedField(field) - .addDeferExecution(deferExecution).build() + .addDeferredExecution(deferredExecution).build() ); } } diff --git a/src/main/java/graphql/execution/MergedField.java b/src/main/java/graphql/execution/MergedField.java index 1ebc53d504..c308b0cc26 100644 --- a/src/main/java/graphql/execution/MergedField.java +++ b/src/main/java/graphql/execution/MergedField.java @@ -3,7 +3,7 @@ import com.google.common.collect.ImmutableList; import graphql.ExperimentalApi; import graphql.PublicApi; -import graphql.execution.incremental.DeferExecution; +import graphql.execution.incremental.DeferredExecution; import graphql.language.Argument; import graphql.language.Field; @@ -64,13 +64,13 @@ public class MergedField { private final ImmutableList fields; private final Field singleField; - private final ImmutableList deferExecutions; + private final ImmutableList deferredExecutions; - private MergedField(ImmutableList fields, ImmutableList deferExecutions) { + private MergedField(ImmutableList fields, ImmutableList deferredExecutions) { assertNotEmpty(fields); this.fields = fields; this.singleField = fields.get(0); - this.deferExecutions = deferExecutions; + this.deferredExecutions = deferredExecutions; } /** @@ -126,13 +126,13 @@ public List getFields() { } /** - * Get a list of all {@link DeferExecution}s that this field is part of + * Get a list of all {@link DeferredExecution}s that this field is part of * * @return all defer executions. */ @ExperimentalApi - public ImmutableList getDeferExecutions() { - return deferExecutions; + public ImmutableList getDeferredExecutions() { + return deferredExecutions; } @@ -157,14 +157,14 @@ public MergedField transform(Consumer builderConsumer) { public static class Builder { private final ImmutableList.Builder fields = new ImmutableList.Builder<>(); - private final ImmutableList.Builder deferExecutions = new ImmutableList.Builder<>(); + private final ImmutableList.Builder deferredExecutions = new ImmutableList.Builder<>(); private Builder() { } private Builder(MergedField existing) { fields.addAll(existing.getFields()); - deferExecutions.addAll(existing.deferExecutions); + deferredExecutions.addAll(existing.deferredExecutions); } public Builder fields(List fields) { @@ -177,20 +177,20 @@ public Builder addField(Field field) { return this; } - public Builder addDeferExecutions(List deferExecutions) { - this.deferExecutions.addAll(deferExecutions); + public Builder addDeferredExecutions(List deferredExecutions) { + this.deferredExecutions.addAll(deferredExecutions); return this; } - public Builder addDeferExecution(@Nullable DeferExecution deferExecution) { - if(deferExecution != null) { - this.deferExecutions.add(deferExecution); + public Builder addDeferredExecution(@Nullable DeferredExecution deferredExecution) { + if(deferredExecution != null) { + this.deferredExecutions.add(deferredExecution); } return this; } public MergedField build() { - return new MergedField(fields.build(), deferExecutions.build()); + return new MergedField(fields.build(), deferredExecutions.build()); } } diff --git a/src/main/java/graphql/execution/incremental/DeferExecution.java b/src/main/java/graphql/execution/incremental/DeferredExecution.java similarity index 88% rename from src/main/java/graphql/execution/incremental/DeferExecution.java rename to src/main/java/graphql/execution/incremental/DeferredExecution.java index fc2c326426..497823a285 100644 --- a/src/main/java/graphql/execution/incremental/DeferExecution.java +++ b/src/main/java/graphql/execution/incremental/DeferredExecution.java @@ -11,10 +11,10 @@ * for the normalized representation of @defer. */ @ExperimentalApi -public class DeferExecution { +public class DeferredExecution { private final String label; - public DeferExecution(String label) { + public DeferredExecution(String label) { this.label = label; } diff --git a/src/main/java/graphql/execution/incremental/IncrementalContext.java b/src/main/java/graphql/execution/incremental/IncrementalCallState.java similarity index 93% rename from src/main/java/graphql/execution/incremental/IncrementalContext.java rename to src/main/java/graphql/execution/incremental/IncrementalCallState.java index 5098b201ea..e3c9622626 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalContext.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCallState.java @@ -21,7 +21,7 @@ * the main result is sent via a Publisher stream. */ @Internal -public class IncrementalContext { +public class IncrementalCallState { private final AtomicBoolean incrementalCallsDetected = new AtomicBoolean(false); private final Deque> incrementalCalls = new ConcurrentLinkedDeque<>(); private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); @@ -70,16 +70,16 @@ private void drainIncrementalCalls() { } public void enqueue(IncrementalCall incrementalCall) { - incrementalCallsDetected.set(true); - incrementalCalls.offer(incrementalCall); - pendingCalls.incrementAndGet(); + publisherLock.runLocked(() -> { + incrementalCallsDetected.set(true); + incrementalCalls.offer(incrementalCall); + pendingCalls.incrementAndGet(); + }); } public void enqueue(Collection> calls) { if (!calls.isEmpty()) { - incrementalCallsDetected.set(true); - incrementalCalls.addAll(calls); - pendingCalls.addAndGet(calls.size()); + calls.forEach(this::enqueue); } } diff --git a/src/main/java/graphql/execution/incremental/IncrementalUtils.java b/src/main/java/graphql/execution/incremental/IncrementalUtils.java index 0781777f35..78d4655027 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalUtils.java +++ b/src/main/java/graphql/execution/incremental/IncrementalUtils.java @@ -19,7 +19,7 @@ public class IncrementalUtils { private IncrementalUtils() {} - public static T createDeferExecution( + public static T createDeferredExecution( Map variables, List directives, Function builderFunction diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 03e45eb5a1..39b35bd501 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -768,7 +768,7 @@ private NormalizedDeferExecution buildDeferExecution( return null; } - return IncrementalUtils.createDeferExecution( + return IncrementalUtils.createDeferredExecution( this.coercedVariableValues.toMap(), directives, (label) -> new NormalizedDeferExecution(label, newPossibleObjects) diff --git a/src/test/groovy/graphql/execution/incremental/IncrementalContextDeferTest.groovy b/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy similarity index 68% rename from src/test/groovy/graphql/execution/incremental/IncrementalContextDeferTest.groovy rename to src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy index b4e53e716f..a2b6e9b57d 100644 --- a/src/test/groovy/graphql/execution/incremental/IncrementalContextDeferTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy @@ -3,9 +3,6 @@ package graphql.execution.incremental import graphql.ExecutionResultImpl import graphql.execution.ResultPath -import graphql.execution.incremental.DeferredCall -import graphql.execution.incremental.DeferredCallContext -import graphql.execution.incremental.IncrementalContext import graphql.incremental.DelayedIncrementalExecutionResult import org.awaitility.Awaitility import spock.lang.Specification @@ -13,17 +10,17 @@ import spock.lang.Specification import java.util.concurrent.CompletableFuture import java.util.function.Supplier -class IncrementalContextDeferTest extends Specification { +class IncrementalCallStateDeferTest extends Specification { def "emits N deferred calls - ordering depends on call latency"() { given: - def incrementalContext = new IncrementalContext() - incrementalContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last - incrementalContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second - incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalCallState = new IncrementalCallState() + incrementalCallState.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + incrementalCallState.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = startAndWaitCalls(incrementalContext) + List results = startAndWaitCalls(incrementalCallState) then: assertResultsSizeAndHasNextRule(3, results) @@ -34,13 +31,13 @@ class IncrementalContextDeferTest extends Specification { def "calls within calls are enqueued correctly"() { given: - def incrementalContext = new IncrementalContext() - incrementalContext.enqueue(offThreadCallWithinCall(incrementalContext, "A", "A_Child", 500, "/a")) - incrementalContext.enqueue(offThreadCallWithinCall(incrementalContext, "B", "B_Child", 300, "/b")) - incrementalContext.enqueue(offThreadCallWithinCall(incrementalContext, "C", "C_Child", 100, "/c")) + def incrementalCallState = new IncrementalCallState() + incrementalCallState.enqueue(offThreadCallWithinCall(incrementalCallState, "A", "A_Child", 500, "/a")) + incrementalCallState.enqueue(offThreadCallWithinCall(incrementalCallState, "B", "B_Child", 300, "/b")) + incrementalCallState.enqueue(offThreadCallWithinCall(incrementalCallState, "C", "C_Child", 100, "/c")) when: - List results = startAndWaitCalls(incrementalContext) + List results = startAndWaitCalls(incrementalCallState) then: assertResultsSizeAndHasNextRule(6, results) @@ -54,10 +51,10 @@ class IncrementalContextDeferTest extends Specification { def "stops at first exception encountered"() { given: - def incrementalContext = new IncrementalContext() - incrementalContext.enqueue(offThread("A", 100, "/field/path")) - incrementalContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception - incrementalContext.enqueue(offThread("C", 10, "/field/path")) + def incrementalCallState = new IncrementalCallState() + incrementalCallState.enqueue(offThread("A", 100, "/field/path")) + incrementalCallState.enqueue(offThread("Bang", 50, "/field/path")) // <-- will throw exception + incrementalCallState.enqueue(offThread("C", 10, "/field/path")) when: def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { @@ -66,7 +63,7 @@ class IncrementalContextDeferTest extends Specification { assert false, "This should not be called!" } } - incrementalContext.startDeferredCalls().subscribe(subscriber) + incrementalCallState.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.isDone()) @@ -80,10 +77,10 @@ class IncrementalContextDeferTest extends Specification { def "you can cancel the subscription"() { given: - def incrementalContext = new IncrementalContext() - incrementalContext.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last - incrementalContext.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second - incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalCallState = new IncrementalCallState() + incrementalCallState.enqueue(offThread("A", 100, "/field/path")) // <-- will finish last + incrementalCallState.enqueue(offThread("B", 50, "/field/path")) // <-- will finish second + incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { @@ -94,7 +91,7 @@ class IncrementalContextDeferTest extends Specification { this.isDone().set(true) } } - incrementalContext.startDeferredCalls().subscribe(subscriber) + incrementalCallState.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.isDone()) def results = subscriber.getEvents() @@ -109,16 +106,16 @@ class IncrementalContextDeferTest extends Specification { def "you can't subscribe twice"() { given: - def incrementalContext = new IncrementalContext() - incrementalContext.enqueue(offThread("A", 100, "/field/path")) - incrementalContext.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second - incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalCallState = new IncrementalCallState() + incrementalCallState.enqueue(offThread("A", 100, "/field/path")) + incrementalCallState.enqueue(offThread("Bang", 50, "/field/path")) // <-- will finish second + incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: def subscriber1 = new graphql.execution.pubsub.CapturingSubscriber() def subscriber2 = new graphql.execution.pubsub.CapturingSubscriber() - incrementalContext.startDeferredCalls().subscribe(subscriber1) - incrementalContext.startDeferredCalls().subscribe(subscriber2) + incrementalCallState.startDeferredCalls().subscribe(subscriber1) + incrementalCallState.startDeferredCalls().subscribe(subscriber2) then: subscriber2.throwable != null @@ -127,17 +124,17 @@ class IncrementalContextDeferTest extends Specification { def "indicates if there are any defers present"() { given: - def incrementalContext = new IncrementalContext() + def incrementalCallState = new IncrementalCallState() when: - def deferPresent1 = incrementalContext.getIncrementalCallsDetected() + def deferPresent1 = incrementalCallState.getIncrementalCallsDetected() then: !deferPresent1 when: - incrementalContext.enqueue(offThread("A", 100, "/field/path")) - def deferPresent2 = incrementalContext.getIncrementalCallsDetected() + incrementalCallState.enqueue(offThread("A", 100, "/field/path")) + def deferPresent2 = incrementalCallState.getIncrementalCallsDetected() then: deferPresent2 @@ -168,10 +165,10 @@ class IncrementalContextDeferTest extends Specification { def deferredCall = new DeferredCall(null, ResultPath.parse("/field/path"), [call1, call2], new DeferredCallContext()) when: - def incrementalContext = new IncrementalContext() - incrementalContext.enqueue(deferredCall) + def incrementalCallState = new IncrementalCallState() + incrementalCallState.enqueue(deferredCall) - def results = startAndWaitCalls(incrementalContext) + def results = startAndWaitCalls(incrementalCallState) then: assertResultsSizeAndHasNextRule(1, results) @@ -181,13 +178,13 @@ class IncrementalContextDeferTest extends Specification { def "race conditions should not impact the calculation of the hasNext value"() { given: "calls that have the same sleepTime" - def incrementalContext = new IncrementalContext() - incrementalContext.enqueue(offThread("A", 10, "/field/path")) // <-- will finish last - incrementalContext.enqueue(offThread("B", 10, "/field/path")) // <-- will finish second - incrementalContext.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first + def incrementalCallState = new IncrementalCallState() + incrementalCallState.enqueue(offThread("A", 10, "/field/path")) // <-- will finish last + incrementalCallState.enqueue(offThread("B", 10, "/field/path")) // <-- will finish second + incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = startAndWaitCalls(incrementalContext) + List results = startAndWaitCalls(incrementalCallState) then: "hasNext placement should be deterministic - only the last event published should have 'hasNext=true'" assertResultsSizeAndHasNextRule(3, results) @@ -215,13 +212,13 @@ class IncrementalContextDeferTest extends Specification { return new DeferredCall(null, ResultPath.parse(path), [callSupplier], new DeferredCallContext()) } - private static DeferredCall offThreadCallWithinCall(IncrementalContext incrementalContext, String dataParent, String dataChild, int sleepTime, String path) { + private static DeferredCall offThreadCallWithinCall(IncrementalCallState incrementalCallState, String dataParent, String dataChild, int sleepTime, String path) { def callSupplier = new Supplier>() { @Override CompletableFuture get() { CompletableFuture.supplyAsync({ Thread.sleep(sleepTime) - incrementalContext.enqueue(offThread(dataChild, sleepTime, path)) + incrementalCallState.enqueue(offThread(dataChild, sleepTime, path)) new DeferredCall.FieldWithExecutionResult(dataParent.toLowerCase(), new ExecutionResultImpl(dataParent, [])) }) } @@ -241,10 +238,10 @@ class IncrementalContextDeferTest extends Specification { } } - private static List startAndWaitCalls(IncrementalContext incrementalContext) { + private static List startAndWaitCalls(IncrementalCallState incrementalCallState) { def subscriber = new graphql.execution.pubsub.CapturingSubscriber() - incrementalContext.startDeferredCalls().subscribe(subscriber) + incrementalCallState.startDeferredCalls().subscribe(subscriber) Awaitility.await().untilTrue(subscriber.isDone()) return subscriber.getEvents() From 1e293af39b1bb6f0923717f28f9a91c5239f7b48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:20:36 +0000 Subject: [PATCH 133/393] Bump gradle/wrapper-validation-action from 1 to 2 Bumps [gradle/wrapper-validation-action](https://github.com/gradle/wrapper-validation-action) from 1 to 2. - [Release notes](https://github.com/gradle/wrapper-validation-action/releases) - [Commits](https://github.com/gradle/wrapper-validation-action/compare/v1...v2) --- updated-dependencies: - dependency-name: gradle/wrapper-validation-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/master.yml | 2 +- .github/workflows/pull_request.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index a7d55e90b0..874fc59329 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/wrapper-validation-action@v2 - name: Set up JDK 11 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ce85c881db..bd64e01bab 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/wrapper-validation-action@v2 - name: Set up JDK 11 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 773bcd66ec..9d2a133472 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/wrapper-validation-action@v2 - name: Set up JDK 11 uses: actions/setup-java@v4 with: From aec85bd8c39c08739bcd369a01acedb3d49e1633 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 6 Feb 2024 09:58:45 +1000 Subject: [PATCH 134/393] data loader tracking --- .../java/graphql/test/agent/AgentTest.java | 16 ---- .../java/graphql/test/agent/TestQuery.java | 44 ----------- .../src/test/java/graphql/test/AgentTest.java | 13 +++- .../src/test/java/graphql/test/TestQuery.java | 77 ++++++++++++++++--- .../java/graphql/agent/GraphQLJavaAgent.java | 51 ++++++++---- 5 files changed, 114 insertions(+), 87 deletions(-) delete mode 100644 agent-test/src/main/java/graphql/test/agent/AgentTest.java delete mode 100644 agent-test/src/main/java/graphql/test/agent/TestQuery.java diff --git a/agent-test/src/main/java/graphql/test/agent/AgentTest.java b/agent-test/src/main/java/graphql/test/agent/AgentTest.java deleted file mode 100644 index c3e3c94a40..0000000000 --- a/agent-test/src/main/java/graphql/test/agent/AgentTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package graphql.test.agent; - -import net.bytebuddy.agent.ByteBuddyAgent; - -import java.io.File; - - -public class AgentTest { - - - public static void main(String[] args) { - ByteBuddyAgent.attach(new File("agent/build/libs/agent.jar"), String.valueOf(ProcessHandle.current().pid())); - TestQuery.executeQuery(); - } - -} diff --git a/agent-test/src/main/java/graphql/test/agent/TestQuery.java b/agent-test/src/main/java/graphql/test/agent/TestQuery.java deleted file mode 100644 index 7a33d65ec1..0000000000 --- a/agent-test/src/main/java/graphql/test/agent/TestQuery.java +++ /dev/null @@ -1,44 +0,0 @@ -package graphql.test.agent; - -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.GraphQL; -import graphql.agent.result.ExecutionTrackingResult; -import graphql.schema.DataFetcher; -import graphql.schema.GraphQLSchema; -import graphql.schema.PropertyDataFetcher; -import graphql.schema.idl.RuntimeWiring; -import graphql.schema.idl.SchemaGenerator; -import graphql.schema.idl.SchemaParser; -import graphql.schema.idl.TypeDefinitionRegistry; - -import java.util.List; -import java.util.Map; - -public class TestQuery { - - static DataFetcher issuesDF = (env) -> { - return List.of( - Map.of("id", "1", "title", "issue-1"), - Map.of("id", "2", "title", "issue-2")); - }; - - static void executeQuery() { - String sdl = "type Query{issues: [Issue]} type Issue {id: ID, title: String}"; - TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(sdl); - PropertyDataFetcher propertyDataFetcher = new PropertyDataFetcher("test"); - - RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() - .type("Query", builder -> builder.dataFetcher("issues", issuesDF)) - .build(); - GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); - - GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); - - ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("{issues{id title}}").build(); - ExecutionResult result = graphQL.execute(executionInput); - System.out.println(result); - ExecutionTrackingResult trackingResult = executionInput.getGraphQLContext().get(ExecutionTrackingResult.EXECUTION_TRACKING_KEY); - System.out.println(trackingResult); - } -} diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java index cdf0fc1ff7..20a4057a0d 100644 --- a/agent-test/src/test/java/graphql/test/AgentTest.java +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -24,6 +24,17 @@ void test() { assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(5); assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); assertThat(executionTrackingResult.getDfResultTypes("/issues")) - .isEqualTo(ExecutionTrackingResult.DFResultType.DONE_OK); + .isEqualTo(ExecutionTrackingResult.DFResultType.DONE_OK); + } + + @Test + void testBatchLoader() { + ExecutionTrackingResult executionTrackingResult = TestQuery.executeBatchedQuery(); + assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(9); + assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); + assertThat(executionTrackingResult.getDfResultTypes("/issues[0]/author")) + .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); + assertThat(executionTrackingResult.getDfResultTypes("/issues[1]/author")) + .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); } } diff --git a/agent-test/src/test/java/graphql/test/TestQuery.java b/agent-test/src/test/java/graphql/test/TestQuery.java index b0ee524a69..43eab5ebad 100644 --- a/agent-test/src/test/java/graphql/test/TestQuery.java +++ b/agent-test/src/test/java/graphql/test/TestQuery.java @@ -6,32 +6,35 @@ import graphql.agent.result.ExecutionTrackingResult; import graphql.schema.DataFetcher; import graphql.schema.GraphQLSchema; -import graphql.schema.PropertyDataFetcher; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; import org.assertj.core.api.Assertions; +import org.dataloader.BatchLoader; +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderFactory; +import org.dataloader.DataLoaderRegistry; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; public class TestQuery { - static DataFetcher issuesDF = (env) -> { - return List.of( - Map.of("id", "1", "title", "issue-1"), - Map.of("id", "2", "title", "issue-2")); - }; static ExecutionTrackingResult executeQuery() { String sdl = "type Query{issues: [Issue]} type Issue {id: ID, title: String}"; TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(sdl); - PropertyDataFetcher propertyDataFetcher = new PropertyDataFetcher("test"); + DataFetcher issuesDF = (env) -> { + return List.of( + Map.of("id", "1", "title", "issue-1"), + Map.of("id", "2", "title", "issue-2")); + }; RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() - .type("Query", builder -> builder.dataFetcher("issues", issuesDF)) - .build(); + .type("Query", builder -> builder.dataFetcher("issues", issuesDF)) + .build(); GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); @@ -42,4 +45,60 @@ static ExecutionTrackingResult executeQuery() { ExecutionTrackingResult trackingResult = executionInput.getGraphQLContext().get(ExecutionTrackingResult.EXECUTION_TRACKING_KEY); return trackingResult; } + + static ExecutionTrackingResult executeBatchedQuery() { + System.out.println("ClassLoader: " + TestQuery.class.getClassLoader()); + String sdl = "type Query{issues: [Issue]} " + + "type Issue {id: ID, author: User}" + + "type User {id: ID, name: String}"; + + DataFetcher issuesDF = (env) -> List.of( + Map.of("id", "1", "title", "issue-1", "authorId", "user-1"), + Map.of("id", "2", "title", "issue-2", "authorId", "user-2")); + + BatchLoader userBatchLoader = keys -> { + System.out.println("batch users with keys: " + keys); + return CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return List.of( + Map.of("id", "user-1", "name", "Foo-1"), + Map.of("id", "user-2", "name", "Foo-2") + ); + }); + }; + DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + dataLoaderRegistry.register("userLoader", DataLoaderFactory.newDataLoader(userBatchLoader)); + + DataFetcher> authorDF = (env) -> { + DataLoader userLoader = env.getDataLoader("userLoader"); + System.out.println("author id: " + (String) ((Map) env.getSource()).get("authorId")); + return userLoader.load((String) ((Map) env.getSource()).get("authorId")); + }; + TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(sdl); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("issues", issuesDF)) + .type("Issue", builder -> builder.dataFetcher("author", authorDF)) + .build(); + GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + String query = "{issues" + + "{id author {id name}}" + + "}"; + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .dataLoaderRegistry(dataLoaderRegistry) + .query(query).build(); + ExecutionResult result = graphQL.execute(executionInput); + Assertions.assertThat(result.getErrors()).isEmpty(); + System.out.println("result: " + result.getData()); + ExecutionTrackingResult trackingResult = executionInput.getGraphQLContext().get(ExecutionTrackingResult.EXECUTION_TRACKING_KEY); + return trackingResult; + } + + } diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index e41b0dcc1d..c1fe708769 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -19,26 +19,43 @@ public class GraphQLJavaAgent { public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("Agent is running"); new AgentBuilder.Default() -// .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) -// .with(AgentBuilder.LambdaInstrumentationStrategy.ENABLED) - .disableClassFormatChanges() - .type(named("graphql.execution.ExecutionStrategy")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - System.out.println("Transforming " + typeDescription); - return builder - .visit(Advice.to(DataFetcherInvokeAdvice.class).on(nameMatches("invokeDataFetcher"))); - }) - .installOn(inst); + .disableClassFormatChanges() + .type(named("graphql.execution.ExecutionStrategy")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + System.out.println("Transforming " + typeDescription); + return builder + .visit(Advice.to(DataFetcherInvokeAdvice.class).on(nameMatches("invokeDataFetcher"))); + }) + .installOn(inst); new AgentBuilder.Default() - .disableClassFormatChanges() - .type(named("graphql.execution.Execution")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - System.out.println("transforming " + typeDescription); - return builder - .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); - }).installOn(inst); + .disableClassFormatChanges() + .type(named("graphql.execution.Execution")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); + }).installOn(inst); + + new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.DISABLED) + .type(named("org.dataloader.DataLoaderRegistry")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(DataLoaderRegistryAdvice.class).on(nameMatches("dispatchAll"))); + }).installOn(inst); + } +} + +class DataLoaderRegistryAdvice { + + @Advice.OnMethodEnter + public static void dispatchAll() { + System.out.println("calling dispatchAll"); } + } class ExecutionAdvice { From 7e6b57961ce8ae399a8f9559d59d1e59d5004db4 Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Wed, 7 Feb 2024 09:47:00 +1000 Subject: [PATCH 135/393] adding defer validation --- .../java/graphql/validation/Validator.java | 12 + .../validation/rules/DeferDirectiveLabel.java | 45 ++ .../rules/DeferDirectiveOnRootLevel.java | 68 +++ .../rules/DeferDirectiveOnValidOperation.java | 63 +++ src/main/resources/i18n/Validation.properties | 5 + .../validation/SpecValidationSchema.java | 24 + .../DeferDirectiveOnRootLevelTest.groovy | 429 ++++++++++++++++++ .../DeferDirectiveOnValidOperationTest.groovy | 252 ++++++++++ .../rules/DeferDirectivesTest.groovy | 148 ++++++ 9 files changed, 1046 insertions(+) create mode 100644 src/main/java/graphql/validation/rules/DeferDirectiveLabel.java create mode 100644 src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java create mode 100644 src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java create mode 100644 src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy create mode 100644 src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy create mode 100644 src/test/groovy/graphql/validation/rules/DeferDirectivesTest.groovy diff --git a/src/main/java/graphql/validation/Validator.java b/src/main/java/graphql/validation/Validator.java index 54558c617a..ed3e292882 100644 --- a/src/main/java/graphql/validation/Validator.java +++ b/src/main/java/graphql/validation/Validator.java @@ -6,6 +6,9 @@ import graphql.language.Document; import graphql.schema.GraphQLSchema; import graphql.validation.rules.ArgumentsOfCorrectType; +import graphql.validation.rules.DeferDirectiveLabel; +import graphql.validation.rules.DeferDirectiveOnRootLevel; +import graphql.validation.rules.DeferDirectiveOnValidOperation; import graphql.validation.rules.UniqueObjectFieldName; import graphql.validation.rules.ExecutableDefinitions; import graphql.validation.rules.FieldsOnCorrectType; @@ -157,6 +160,15 @@ public List createRules(ValidationContext validationContext, Valid UniqueObjectFieldName uniqueObjectFieldName = new UniqueObjectFieldName(validationContext, validationErrorCollector); rules.add(uniqueObjectFieldName); + DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector); + rules.add(deferDirectiveLabel); + + DeferDirectiveOnRootLevel deferDirectiveOnRootLevel = new DeferDirectiveOnRootLevel(validationContext, validationErrorCollector); + rules.add(deferDirectiveOnRootLevel); + + DeferDirectiveOnValidOperation deferDirectiveOnValidOperation = new DeferDirectiveOnValidOperation(validationContext, validationErrorCollector); + rules.add(deferDirectiveOnValidOperation); + return rules; } } diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java new file mode 100644 index 0000000000..d4a204dc0b --- /dev/null +++ b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java @@ -0,0 +1,45 @@ +package graphql.validation.rules; + +import graphql.Internal; +import graphql.language.Argument; +import graphql.language.Directive; +import graphql.language.Node; +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.validation.AbstractRule; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationErrorCollector; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import static graphql.validation.ValidationErrorType.DuplicateArgumentNames; + +@Internal +public class DeferDirectiveLabel extends AbstractRule { + + private Set labels = new LinkedHashSet<>(); + public DeferDirectiveLabel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + } + + @Override + public void checkDirective(Directive directive, List ancestors) { + if (!directive.getName().equals("defer") || directive.getArguments().size() == 0) { + return; + } + + Argument labelArgument = directive.getArgument("label"); + // argument type is validated in DeferDirectiveArgumentType + if (labelArgument != null && labelArgument.getValue() instanceof StringValue) { + if (labels.contains(((StringValue) labelArgument.getValue()).getValue())) { + String message = i18n(DuplicateArgumentNames, "UniqueArgumentNames.directiveUniqueArgument", labelArgument.getName(), directive.getName()); + addError(DuplicateArgumentNames, directive.getSourceLocation(), message); + } else { + labels.add(((StringValue) labelArgument.getValue()).getValue() ); + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java new file mode 100644 index 0000000000..d8a989d092 --- /dev/null +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java @@ -0,0 +1,68 @@ +package graphql.validation.rules; + +import graphql.language.Directive; +import graphql.language.Document; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.InlineFragment; +import graphql.language.Node; +import graphql.language.OperationDefinition; +import graphql.language.SelectionSet; +import graphql.validation.AbstractRule; +import graphql.validation.DocumentVisitor; +import graphql.validation.LanguageTraversal; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationErrorCollector; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static graphql.validation.ValidationErrorType.MisplacedDirective; + +public class DeferDirectiveOnRootLevel extends AbstractRule { + + public DeferDirectiveOnRootLevel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + this.setVisitFragmentSpreads(true); + } + + @Override + public void checkDirective(Directive directive, List ancestors) { + if (directive.getName().equals("defer")){ + Optional fragmentAncestor = getFragmentAncestor(ancestors); + if (fragmentAncestor.isPresent() && fragmentAncestor.get() instanceof OperationDefinition){ + OperationDefinition operationDefinition = (OperationDefinition) fragmentAncestor.get(); + + if (OperationDefinition.Operation.MUTATION.equals(operationDefinition.getOperation())) { + String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), OperationDefinition.Operation.MUTATION.name().toLowerCase()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } else if (OperationDefinition.Operation.SUBSCRIPTION.equals(operationDefinition.getOperation())) { + String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), OperationDefinition.Operation.SUBSCRIPTION.name().toLowerCase()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } + } + + } + } + + /** + * Get the first ancestor that is not InlineFragment, SelectionSet or FragmentDefinition + * @param ancestors list of ancestors + * @return Optional of Node parent that is not InlineFragment, SelectionSet or FragmentDefinition. + */ + protected Optional getFragmentAncestor(List ancestors){ + List ancestorsCopy = new ArrayList(ancestors); + Collections.reverse(ancestorsCopy); + return ancestorsCopy.stream().filter(node -> !( + node instanceof InlineFragment || + node instanceof SelectionSet || + node instanceof FragmentDefinition + ) + ).findFirst(); + + } +} diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java new file mode 100644 index 0000000000..457a58f0ea --- /dev/null +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java @@ -0,0 +1,63 @@ +package graphql.validation.rules; + +import graphql.language.BooleanValue; +import graphql.language.Directive; +import graphql.language.Document; +import graphql.language.Node; +import graphql.language.OperationDefinition; +import graphql.validation.AbstractRule; +import graphql.validation.ValidationContext; +import graphql.validation.ValidationErrorCollector; + +import java.util.List; +import java.util.Optional; + +import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; +import static graphql.validation.ValidationErrorType.MisplacedDirective; + +/** + * Defer Directive is Used On Valid Operations + * + * A GraphQL document is only valid if defer directives are not used on subscription types. + */ +public class DeferDirectiveOnValidOperation extends AbstractRule { + + public DeferDirectiveOnValidOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { + super(validationContext, validationErrorCollector); + this.setVisitFragmentSpreads(true); + } + + @Override + public void documentFinished(Document document) { + super.documentFinished(document); + } + + @Override + public void checkDirective(Directive directive, List ancestors) { + if (!directive.getName().equals("defer") || + (directive.getArgumentsByName().get("if") != null && !((BooleanValue) directive.getArgumentsByName().get("if").getValue()).isValue() )) { + return; + } + // check if the directive is on allowed operation + Optional operationDefinition = getOperation(ancestors); + if (operationDefinition.isPresent() && operationDefinition.get().getOperation() == SUBSCRIPTION) { + String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperation", directive.getName(), SUBSCRIPTION.name().toLowerCase()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } + } + + + /** + * Extract from ancestors the OperationDefinition using the document ancestor. + * @param ancestors list of ancestors + * @return OperationDefinition + */ + protected Optional getOperation(List ancestors) { + return ancestors.stream() + .filter(doc -> doc instanceof OperationDefinition) + .map((def -> (OperationDefinition) def)) + .findFirst(); + } + +} + diff --git a/src/main/resources/i18n/Validation.properties b/src/main/resources/i18n/Validation.properties index 82c754db3e..2e94555ee7 100644 --- a/src/main/resources/i18n/Validation.properties +++ b/src/main/resources/i18n/Validation.properties @@ -63,6 +63,8 @@ SubscriptionIntrospectionRootField.introspectionRootField=Validation error ({0}) SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validation error ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' cannot be an introspection field # UniqueArgumentNames.uniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}'' + +UniqueArgumentNames.directiveUniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}'' for directive ''{2}'' # UniqueDirectiveNamesPerLocation.uniqueDirectives=Validation error ({0}) : Non repeatable directives must be uniquely named within a location. The directive ''{1}'' used on a ''{2}'' is not unique # @@ -98,3 +100,6 @@ ArgumentValidationUtil.handleNotObjectError=Validation error ({0}) : argument '' ArgumentValidationUtil.handleMissingFieldsError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' is missing required fields ''{3}'' # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleExtraFieldError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' contains a field not in ''{3}'': ''{4}'' + +DirectiveMisplaced.notAllowedOperationRootLevel=Validation error ({0}) : Directive ''{1}'' is not allowed on root of operation ''{2}'' +DirectiveMisplaced.notAllowedOperation=Validation error ({0}) : Directive ''{1}'' is not on operation ''{2}'' \ No newline at end of file diff --git a/src/test/groovy/graphql/validation/SpecValidationSchema.java b/src/test/groovy/graphql/validation/SpecValidationSchema.java index 45a6b2637f..060a2399cd 100644 --- a/src/test/groovy/graphql/validation/SpecValidationSchema.java +++ b/src/test/groovy/graphql/validation/SpecValidationSchema.java @@ -30,7 +30,10 @@ import static graphql.introspection.Introspection.DirectiveLocation.QUERY; import static graphql.schema.GraphQLArgument.newArgument; import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; +import static graphql.schema.GraphQLInputObjectField.newInputObjectField; +import static graphql.schema.GraphQLInputObjectType.newInputObject; import static graphql.schema.GraphQLNonNull.nonNull; +import static graphql.schema.GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY; import static java.util.Collections.singletonList; /** @@ -215,6 +218,25 @@ public class SpecValidationSchema { .field(newFieldDefinition().name("cat").type(cat)) .build(); + public static GraphQLInputObjectType inputDogType = newInputObject() + .name("DogInput") + .description("Input for A Dog creation.") + .field(newInputObjectField() + .name("id") + .description("The id of the dog.") + .type(nonNull(GraphQLString))) + .build(); + + public static final GraphQLObjectType petMutationType = GraphQLObjectType.newObject() + .name("PetMutationType") + .field(newFieldDefinition() + .name("createDog") + .type(dog) + .argument(newArgument() + .name("input") + .type(inputDogType))) + .build(); + public static final Set specValidationDictionary = new HashSet() {{ add(dogCommand); add(catCommand); @@ -275,11 +297,13 @@ public class SpecValidationSchema { .query(queryRoot) .codeRegistry(codeRegistry) .subscription(subscriptionRoot) + .mutation(petMutationType) .additionalDirective(upperDirective) .additionalDirective(lowerDirective) .additionalDirective(dogDirective) .additionalDirective(nonNullDirective) .additionalDirective(objectArgumentDirective) + .additionalDirective(Directives.DeferDirective) .additionalTypes(specValidationDictionary) .build(); diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy new file mode 100644 index 0000000000..c707a9d949 --- /dev/null +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy @@ -0,0 +1,429 @@ +package graphql.validation.rules + +import graphql.TestUtil +import graphql.i18n.I18n +import graphql.language.Document +import graphql.parser.Parser +import graphql.validation.LanguageTraversal +import graphql.validation.RulesVisitor +import graphql.validation.SpecValidationSchema +import graphql.validation.TraversalContext +import graphql.validation.ValidationContext +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorCollector +import graphql.validation.ValidationErrorType +import graphql.validation.Validator +import spock.lang.Specification + +class DeferDirectiveOnRootLevelTest extends Specification { + + ValidationContext validationContext = Mock(ValidationContext) + ValidationErrorCollector errorCollector = new ValidationErrorCollector() + + DeferDirectiveOnRootLevel deferDirectiveOnRootLevel = new DeferDirectiveOnRootLevel(validationContext, errorCollector) + + def traverse(String query) { + Document document = new Parser().parseDocument(query) + I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) + ValidationContext validationContext = new ValidationContext(SpecValidationSchema.specValidationSchema, document, i18n) + LanguageTraversal languageTraversal = new LanguageTraversal() + languageTraversal.traverse(document, new RulesVisitor(validationContext, [new DeferDirectiveOnRootLevel(validationContext, errorCollector)])) + } + + def setup() { + def traversalContext = Mock(TraversalContext) + validationContext.getSchema() >> SpecValidationSchema.specValidationSchema + validationContext.getTraversalContext() >> traversalContext + } + + def "Not allow defer on subscription root level"() { + given: + def query = """ + subscription pets { + ... @defer { + dog { + name + } + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + + then: + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + + } + + def "Not allow defer mutation root level "() { + given: + def query = """ + mutation dog { + ... @defer { + createDog(input: {id: "1"}) { + name + } + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + validationErrors.get(0).message == "Validation error (MisplacedDirective) : Directive 'defer' is not allowed on root of operation 'mutation'" + + } + + def "Defer directive is allowed on query root level"() { + given: + def query = """ + query defer_query { + ... @defer { + dog { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.isEmpty() + } + + def "allow defer on when is not on mutation root level"() { + given: + def query = """ + mutation doggo { + createDog(id: "1") { + ... @defer { + id + } + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + + then: + errorCollector.errors.isEmpty() + } + + def "Not allow defer mutation root level on inline fragments "() { + given: + def query = """ + + mutation doggo { + ... { + ... @defer { + createDog(input: {id: "1"}) { + name + } + } + + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + validationErrors.get(0).message == "Validation error (MisplacedDirective) : Directive 'defer' is not allowed on root of operation 'mutation'" + + } + + def "Not allow defer on subscription root level even when is inside multiple inline fragment"() { + given: + def query = """ + subscription pets { + ...{ + ...{ + ... @defer { + dog { + name + } + } + } + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + + then: + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + + } + + + def "Not allow defer on mutation root level even when ih multiple inline fragments split in fragment"() { + given: + def query = """ + fragment doggo on PetMutationType { + ... { + ... @defer { + createDog(id: "1") { + id + } + } + } + } + + mutation doggoMutation { + ...{ + ...doggo + } + } + + + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + traverse(query) + + then: + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + + } + + + + + def "Allows defer on mutation when it is not on root level"() { + given: + def query = """ + mutation pets { + createDog(input: {id: "1"}) { + ... @defer { + name + } + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + + then: + errorCollector.errors.isEmpty() + } + + def "allow defer on fragment when is not on mutation root level"() { + given: + def query = """ + mutation doggo { + ...{ + ...doggoCreate + } + } + + fragment doggoCreate on PetMutationType { + createDog(id: "1") { + ... @defer { + id + } + } + } + + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + + then: + errorCollector.errors.isEmpty() + } + + + def "allow defer on split fragment when is not on mutation root level"() { + given: + def query = """ + mutation doggo { + ...doggoCreate + } + + fragment doggoCreate on PetMutationType { + createDog(id: "1") { + ...doggoFields + } + } + + fragment doggoFields on Dog { + ... @defer { + id + } + } + + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + + then: + errorCollector.errors.isEmpty() + + + } + + + def "Not allow defer subscription root level even when there are multiple subscriptions"() { + given: + def query = """ + subscription pets { + dog { + name + } + } + subscription dog { + ... @defer { + dog { + name + } + } + } + + subscription morePets { + cat { + name + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + + then: + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.size() == 1 + + } + + def "Not allow defer on mutation root level when there are multiple fragment levels regarless fragment order on query"() { + given: + def query = """ + + fragment createDoggoRoot on PetMutationType { + ... { + ...createDoggo + } + } + + mutation createDoggoRootOp { + ...createDoggoRoot + } + + fragment createDoggo on PetMutationType { + ... { + ... @defer { + createDog(input: {id: "1"}) { + name + } + } + } + } + + """ + + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + validationErrors.get(0).message == "Validation error (MisplacedDirective@[createDoggoRoot/createDoggo]) : Directive 'defer' is not allowed on root of operation 'mutation'" + + } + + def "Not allow defer on mutation root level even when there are multiple fragments and operations"() { + given: + def query = """ + + fragment createDoggoLevel1 on PetMutationType { + ... { + ... { + ...createDoggoLevel2 + } + } + } + + fragment createDoggoLevel2 on PetMutationType { + ...createDoggo + } + + fragment createDoggo on PetMutationType { + ... { + ... @defer { + createDog(input: {id: "1"}) { + name + } + } + } + } + + query pets1 { + ... @defer { + dog { + name + } + } + } + + mutation createDoggo { + ...createDoggoLevel1 + } + + """ + + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + validationErrors.get(0).message == "Validation error (MisplacedDirective@[createDoggoLevel1/createDoggoLevel2/createDoggo]) : Directive 'defer' is not allowed on root of operation 'mutation'" + + } + + static List validate(String query) { + def document = new Parser().parseDocument(query) + return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + } +} + diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy new file mode 100644 index 0000000000..b62578db31 --- /dev/null +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy @@ -0,0 +1,252 @@ +package graphql.validation.rules + +import graphql.i18n.I18n +import graphql.language.Document +import graphql.parser.Parser +import graphql.validation.LanguageTraversal +import graphql.validation.RulesVisitor +import graphql.validation.SpecValidationSchema +import graphql.validation.TraversalContext +import graphql.validation.ValidationContext +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorCollector +import graphql.validation.ValidationErrorType +import graphql.validation.Validator +import spock.lang.Specification + +class DeferDirectiveOnValidOperationTest extends Specification { + + ValidationContext validationContext = Mock(ValidationContext) + ValidationErrorCollector errorCollector = new ValidationErrorCollector() + + def traverse(String query) { + Document document = new Parser().parseDocument(query) + I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) + ValidationContext validationContext = new ValidationContext(SpecValidationSchema.specValidationSchema, document, i18n) + LanguageTraversal languageTraversal = new LanguageTraversal() + languageTraversal.traverse(document, new RulesVisitor(validationContext, [new DeferDirectiveOnValidOperation(validationContext, errorCollector)])) + } + + def setup() { + def traversalContext = Mock(TraversalContext) + validationContext.getSchema() >> SpecValidationSchema.specValidationSchema + validationContext.getTraversalContext() >> traversalContext + } + + + + def "Not allow defer on subscription operation"() { + given: + def query = """ + subscription pets { + dog { + ... @defer { + name + } + nickname + } + } + """ + + + when: + traverse(query) + + then: + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + + } + + + def "Allow defer(if:false) on subscription operation"() { + given: + def query = """ + subscription pets { + dog { + ... @defer(if:false) { + name + } + nickname + } + } + """ + + + when: + traverse(query) + + then: + errorCollector.errors.isEmpty() + + } + + + def "Not allow defer on fragment when operation is subscription"() { + given: + def query = """ + fragment doggo on PetMutationType { + ... { + dog { + ... @defer { + id + } + nickname + } + + } + } + + subscription doggoMutation { + ...{ + ...doggo + } + } + + + """ + when: + traverse(query) + + then: + + then: + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + + } + + def "Allow defer(if:false) on fragment when operation is subscription"() { + given: + def query = """ + fragment doggo on PetMutationType { + ... { + dog { + ... @defer(if:false) { + id + } + nickname + } + + } + } + + subscription doggoMutation { + ...{ + ...doggo + } + } + + + """ + when: + traverse(query) + + then: + errorCollector.errors.isEmpty() + + } + + def "Not allow defer subscription root level even when there are multiple operations"() { + given: + def query = """ + query pets { + ... @defer { + dog { + name + } + } + } + + subscription pets2 { + dog { + ... @defer { + name + } + } + } + + query pets3 { + dog { + name + } + } + """ + + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + validationErrors.get(0).message == "Validation error (MisplacedDirective@[dog]) : Directive 'defer' is not on operation 'subscription'" + + } + + + def "Allows defer on mutation when it is not on root level"() { + given: + def query = """ + mutation pets { + dog { + ... @defer { + name + } + } + } + """ + when: + traverse(query) + + then: + errorCollector.errors.isEmpty() + + } + + def "Defer directive Label must be string"() { + given: + def query = """ + query defer_query { + dog { + ... @defer(label: 1) { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'label' with value 'IntValue{value=1}' is not a valid 'String' - Expected an AST type of 'StringValue' but it was a 'IntValue'" + } + + + def "Defer directive is allowed on query root level"() { + given: + def query = """ + query defer_query { + ... @defer { + dog { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.isEmpty() + } + + static List validate(String query) { + def document = new Parser().parseDocument(query) + return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + } +} + diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectivesTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectivesTest.groovy new file mode 100644 index 0000000000..9d88117457 --- /dev/null +++ b/src/test/groovy/graphql/validation/rules/DeferDirectivesTest.groovy @@ -0,0 +1,148 @@ +package graphql.validation.rules + +import graphql.language.Document +import graphql.parser.Parser +import graphql.validation.LanguageTraversal +import graphql.validation.RulesVisitor +import graphql.validation.SpecValidationSchema +import graphql.validation.TraversalContext +import graphql.validation.ValidationContext +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorCollector +import graphql.validation.ValidationErrorType +import graphql.validation.Validator +import spock.lang.Specification + +class DeferDirectivesTest extends Specification { + + ValidationContext validationContext = Mock(ValidationContext) + ValidationErrorCollector errorCollector = new ValidationErrorCollector() + + DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, errorCollector) + + def setup() { + def traversalContext = Mock(TraversalContext) + validationContext.getSchema() >> SpecValidationSchema.specValidationSchema + validationContext.getTraversalContext() >> traversalContext + } + + def "Allow unique label directive"() { + given: + def query = """ + query defer_query { + ... @defer(label: "name") { + human { + name + } + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + + then: + errorCollector.errors.isEmpty() + + } + + def "Defer directive label argument must be unique"() { + given: + def query = """ + query defer_query { + dog { + ... @defer(label: "name") { + name + } + } + alien { + ... @defer(label: "name") { + name + } + } + + } + """ + + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + + then: + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.DuplicateArgumentNames) + } + + def "Multiple use of Defer directive is valid"() { + given: + def query = """ + query defer_query { + dog { + ... @defer { + name + } + ... @defer { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.isEmpty() + } + + def "Multiple use of Defer directive with different labels is valid"() { + given: + def query = """ + query defer_query { + dog { + ... @defer(label: "name") { + name + } + ... @defer(label: "nameAgain") { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.isEmpty() + } + + def "Defer directive Label must be string"() { + given: + def query = """ + query defer_query { + dog { + ... @defer(label: 1) { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'label' with value 'IntValue{value=1}' is not a valid 'String' - Expected an AST type of 'StringValue' but it was a 'IntValue'" + } + + static List validate(String query) { + def document = new Parser().parseDocument(query) + return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + } +} + From 2f2b794fdf65992ea028ad46a524cb5a0ce64ba2 Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Wed, 7 Feb 2024 11:27:23 +1000 Subject: [PATCH 136/393] Cleaning up the code. --- .../java/graphql/validation/rules/DeferDirectiveLabel.java | 2 -- .../graphql/validation/rules/DeferDirectiveOnRootLevel.java | 6 ------ .../validation/rules/DeferDirectiveOnValidOperation.java | 1 - .../validation/rules/DeferDirectiveOnRootLevelTest.groovy | 1 - 4 files changed, 10 deletions(-) diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java index d4a204dc0b..9214a592da 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java @@ -5,11 +5,9 @@ import graphql.language.Directive; import graphql.language.Node; import graphql.language.StringValue; -import graphql.language.Value; import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; - import java.util.LinkedHashSet; import java.util.List; import java.util.Set; diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java index d8a989d092..dacae21582 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java @@ -1,24 +1,18 @@ package graphql.validation.rules; import graphql.language.Directive; -import graphql.language.Document; import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; import graphql.language.InlineFragment; import graphql.language.Node; import graphql.language.OperationDefinition; import graphql.language.SelectionSet; import graphql.validation.AbstractRule; -import graphql.validation.DocumentVisitor; -import graphql.validation.LanguageTraversal; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import static graphql.validation.ValidationErrorType.MisplacedDirective; diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java index 457a58f0ea..ea65097b54 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java @@ -8,7 +8,6 @@ import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; - import java.util.List; import java.util.Optional; diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy index c707a9d949..cce20c3c4d 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy @@ -1,6 +1,5 @@ package graphql.validation.rules -import graphql.TestUtil import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser From 63e344e7c47c1a87c0aed8f70dd9e06a07bdd88d Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Wed, 7 Feb 2024 11:41:36 +1000 Subject: [PATCH 137/393] Clean up code --- .../validation/rules/DeferDirectiveOnRootLevel.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java index dacae21582..7a4cd1a70d 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java @@ -30,12 +30,8 @@ public void checkDirective(Directive directive, List ancestors) { Optional fragmentAncestor = getFragmentAncestor(ancestors); if (fragmentAncestor.isPresent() && fragmentAncestor.get() instanceof OperationDefinition){ OperationDefinition operationDefinition = (OperationDefinition) fragmentAncestor.get(); - - if (OperationDefinition.Operation.MUTATION.equals(operationDefinition.getOperation())) { - String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), OperationDefinition.Operation.MUTATION.name().toLowerCase()); - addError(MisplacedDirective, directive.getSourceLocation(), message); - } else if (OperationDefinition.Operation.SUBSCRIPTION.equals(operationDefinition.getOperation())) { - String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), OperationDefinition.Operation.SUBSCRIPTION.name().toLowerCase()); + if (OperationDefinition.Operation.MUTATION.equals(operationDefinition.getOperation()) || OperationDefinition.Operation.SUBSCRIPTION.equals(operationDefinition.getOperation())) { + String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), operationDefinition.getOperation().name().toLowerCase()); addError(MisplacedDirective, directive.getSourceLocation(), message); } } From 4655c4a397149b8754b5115e9e05a300f3818c83 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 7 Feb 2024 11:55:30 +1000 Subject: [PATCH 138/393] wip --- .../java/graphql/agent/GraphQLJavaAgent.java | 103 +++++++++++++++--- 1 file changed, 88 insertions(+), 15 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index c1fe708769..178f23186f 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -2,13 +2,20 @@ import graphql.agent.result.ExecutionTrackingResult; import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionId; import graphql.execution.ExecutionStrategyParameters; import graphql.execution.ResultPath; +import graphql.schema.DataFetchingEnvironment; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import org.dataloader.DataLoader; import java.lang.instrument.Instrumentation; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import static graphql.agent.result.ExecutionTrackingResult.EXECUTION_TRACKING_KEY; import static net.bytebuddy.matcher.ElementMatchers.nameMatches; @@ -16,43 +23,101 @@ public class GraphQLJavaAgent { + public static class ExecutionData { + public Set resultPaths = ConcurrentHashMap.newKeySet(); + + public Map resultPathToDataLoader = new ConcurrentHashMap<>(); + public Map dataLoaderToName = new ConcurrentHashMap<>(); + + @Override + public String toString() { + return "ExecutionData{" + + "resultPathToDataLoader=" + resultPathToDataLoader + + ", dataLoaderToName=" + dataLoaderToName + + '}'; + } + } + + public static final Map executionDataMap = new ConcurrentHashMap<>(); + public static final Map dataLoaderToExecutionId = new ConcurrentHashMap<>(); + public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("Agent is running"); new AgentBuilder.Default() - .disableClassFormatChanges() + .type(named("graphql.execution.Execution")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); + + }) .type(named("graphql.execution.ExecutionStrategy")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { System.out.println("Transforming " + typeDescription); return builder .visit(Advice.to(DataFetcherInvokeAdvice.class).on(nameMatches("invokeDataFetcher"))); }) - .installOn(inst); - - new AgentBuilder.Default() - .disableClassFormatChanges() - .type(named("graphql.execution.Execution")) + .type(named("org.dataloader.DataLoaderRegistry")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { System.out.println("transforming " + typeDescription); return builder - .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); - }).installOn(inst); - - new AgentBuilder.Default() - .disableClassFormatChanges() - .with(AgentBuilder.RedefinitionStrategy.DISABLED) - .type(named("org.dataloader.DataLoaderRegistry")) + .visit(Advice.to(DataLoaderRegistryAdvice.class).on(nameMatches("dispatchAll"))); + }) + .type(named("org.dataloader.DataLoader")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { System.out.println("transforming " + typeDescription); return builder - .visit(Advice.to(DataLoaderRegistryAdvice.class).on(nameMatches("dispatchAll"))); - }).installOn(inst); + .visit(Advice.to(DataLoaderAdvice.class).on(nameMatches("load"))); + }) + .type(named("graphql.schema.DataFetchingEnvironmentImpl")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(DataFetchingEnvironmentAdvice.class).on(nameMatches("getDataLoader"))); + }) + .installOn(inst); + } } +class DataFetchingEnvironmentAdvice { + + + @Advice.OnMethodExit + public static void getDataLoader(@Advice.Argument(0) String dataLoaderName, + @Advice.This(typing = Assigner.Typing.DYNAMIC) DataFetchingEnvironment dataFetchingEnvironment, + @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) DataLoader dataLoader) { + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionDataMap.get(dataFetchingEnvironment.getExecutionId()); + System.out.println("execution data: " + executionData); + executionData.resultPathToDataLoader.put(dataFetchingEnvironment.getExecutionStepInfo().getPath(), dataLoader); + executionData.dataLoaderToName.put(dataLoader, dataLoaderName); + GraphQLJavaAgent.dataLoaderToExecutionId.put(dataLoader, dataFetchingEnvironment.getExecutionId()); + + System.out.println(dataLoaderName + " > " + dataLoader); + } + +} + + +class DataLoaderAdvice { + + @Advice.OnMethodEnter + public static void load(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoader) { + ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); + String dataLoaderName = GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.get(dataLoader); + System.out.println("dataloader " + dataLoaderName + " load for execution " + executionId); + } + +} + class DataLoaderRegistryAdvice { @Advice.OnMethodEnter public static void dispatchAll() { + // StackWalker instance = StackWalker.getInstance(); + // instance.forEach(stackFrame -> { + // System.out.println(stackFrame.getClassName() + " " + stackFrame.getMethodName() + " " + stackFrame.getLineNumber()); + // }); System.out.println("calling dispatchAll"); } @@ -62,7 +127,14 @@ class ExecutionAdvice { @Advice.OnMethodEnter public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { + System.out.println("execution started for: " + executionContext.getExecutionId()); executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); + GraphQLJavaAgent.executionDataMap.put(executionContext.getExecutionId(), new GraphQLJavaAgent.ExecutionData()); + } + + @Advice.OnMethodExit + public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext) { + System.out.println("execution finished for: " + executionContext.getExecutionId()); } } @@ -70,6 +142,7 @@ class DataFetcherInvokeAdvice { @Advice.OnMethodEnter public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext executionContext, @Advice.Argument(1) ExecutionStrategyParameters parameters) { + ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); executionTrackingResult.start(parameters.getPath(), System.nanoTime()); } From affc1b827c06c7e1f8be8a634ee8a181a1fc2dc4 Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Wed, 7 Feb 2024 12:14:04 +1000 Subject: [PATCH 139/393] Cleaning up code and test --- .../graphql/validation/rules/DeferDirectiveLabel.java | 2 -- .../validation/rules/DeferDirectiveOnRootLevel.java | 1 - .../validation/rules/DeferDirectiveOnValidOperation.java | 8 +------- .../validation/rules/UniqueOperationNamesTest.groovy | 4 ++-- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java index 9214a592da..37ab679945 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java @@ -16,7 +16,6 @@ @Internal public class DeferDirectiveLabel extends AbstractRule { - private Set labels = new LinkedHashSet<>(); public DeferDirectiveLabel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { super(validationContext, validationErrorCollector); @@ -27,7 +26,6 @@ public void checkDirective(Directive directive, List ancestors) { if (!directive.getName().equals("defer") || directive.getArguments().size() == 0) { return; } - Argument labelArgument = directive.getArgument("label"); // argument type is validated in DeferDirectiveArgumentType if (labelArgument != null && labelArgument.getValue() instanceof StringValue) { diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java index 7a4cd1a70d..c4544d8f17 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java @@ -53,6 +53,5 @@ protected Optional getFragmentAncestor(List ancestors){ node instanceof FragmentDefinition ) ).findFirst(); - } } diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java index ea65097b54..f506f1c49b 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java @@ -26,11 +26,6 @@ public DeferDirectiveOnValidOperation(ValidationContext validationContext, Valid this.setVisitFragmentSpreads(true); } - @Override - public void documentFinished(Document document) { - super.documentFinished(document); - } - @Override public void checkDirective(Directive directive, List ancestors) { if (!directive.getName().equals("defer") || @@ -45,13 +40,12 @@ public void checkDirective(Directive directive, List ancestors) { } } - /** * Extract from ancestors the OperationDefinition using the document ancestor. * @param ancestors list of ancestors * @return OperationDefinition */ - protected Optional getOperation(List ancestors) { + private Optional getOperation(List ancestors) { return ancestors.stream() .filter(doc -> doc instanceof OperationDefinition) .map((def -> (OperationDefinition) def)) diff --git a/src/test/groovy/graphql/validation/rules/UniqueOperationNamesTest.groovy b/src/test/groovy/graphql/validation/rules/UniqueOperationNamesTest.groovy index 1fa03eb24c..c5fa1eb3d2 100644 --- a/src/test/groovy/graphql/validation/rules/UniqueOperationNamesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/UniqueOperationNamesTest.groovy @@ -46,8 +46,8 @@ class UniqueOperationNamesTest extends Specification { } mutation dogOperation { - mutateDog { - id + createDog(input: {id: "1"}) { + name } } """.stripIndent() From 0c8ed1d46603ecc3bd278a8224611b63f6a92415 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:57:51 +1100 Subject: [PATCH 140/393] Skip signing for local and upgrade to Gradle 8.6 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 0356d56b93..6c61a0c167 100644 --- a/build.gradle +++ b/build.gradle @@ -347,8 +347,8 @@ nexusPublishing { } } -// to publish to local maven repo skip signing: ./gradlew publishToMavenLocal -x signGraphqlJavaPublication signing { + required { !version.endsWith('-SNAPSHOT') } def signingKey = System.env.MAVEN_CENTRAL_PGP_KEY useInMemoryPgpKeys(signingKey, "") sign publishing.publications diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e0930b..a80b22ce5c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 10ff5206358ed72d7a6b6f4389bad7dfae2c4abd Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Wed, 7 Feb 2024 13:28:58 +1000 Subject: [PATCH 141/393] Improving tests --- src/main/resources/i18n/Validation.properties | 2 +- .../DeferDirectiveOnValidOperationTest.groovy | 81 ++++++++++--------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/main/resources/i18n/Validation.properties b/src/main/resources/i18n/Validation.properties index 2e94555ee7..e5921e9746 100644 --- a/src/main/resources/i18n/Validation.properties +++ b/src/main/resources/i18n/Validation.properties @@ -102,4 +102,4 @@ ArgumentValidationUtil.handleMissingFieldsError=Validation error ({0}) : argumen ArgumentValidationUtil.handleExtraFieldError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' contains a field not in ''{3}'': ''{4}'' DirectiveMisplaced.notAllowedOperationRootLevel=Validation error ({0}) : Directive ''{1}'' is not allowed on root of operation ''{2}'' -DirectiveMisplaced.notAllowedOperation=Validation error ({0}) : Directive ''{1}'' is not on operation ''{2}'' \ No newline at end of file +DirectiveMisplaced.notAllowedOperation=Validation error ({0}) : Directive ''{1}'' is not allowed on operation ''{2}'' \ No newline at end of file diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy index b62578db31..1e6dfa5525 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy @@ -147,9 +147,18 @@ class DeferDirectiveOnValidOperationTest extends Specification { } - def "Not allow defer subscription root level even when there are multiple operations"() { + def "Not allow defer subscription even when there are multiple operations with multiple fragments"() { given: def query = """ + + fragment doggoSubscription on SubscriptionRoot { + ... { + dog { + ...doggo + } + } + } + query pets { ... @defer { dog { @@ -159,11 +168,7 @@ class DeferDirectiveOnValidOperationTest extends Specification { } subscription pets2 { - dog { - ... @defer { - name - } - } + ...doggoSubscription } query pets3 { @@ -171,6 +176,12 @@ class DeferDirectiveOnValidOperationTest extends Specification { name } } + + fragment doggo on Dog{ + ... @defer { + name + } + } """ when: @@ -180,68 +191,60 @@ class DeferDirectiveOnValidOperationTest extends Specification { !validationErrors.isEmpty() validationErrors.size() == 1 validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective - validationErrors.get(0).message == "Validation error (MisplacedDirective@[dog]) : Directive 'defer' is not on operation 'subscription'" + validationErrors.get(0).message == "Validation error (MisplacedDirective@[doggoSubscription/dog/doggo]) : Directive 'defer' is not allowed on operation 'subscription'" } - def "Allows defer on mutation when it is not on root level"() { + def "Not allow defer subscription even when there are multiple operations and multiple fragments"() { given: def query = """ - mutation pets { + query pets { + ... @defer { + dog { + name + } + } + } + + subscription pets2 { dog { ... @defer { name } } - } + } + + """ - when: - traverse(query) - - then: - errorCollector.errors.isEmpty() - - } - def "Defer directive Label must be string"() { - given: - def query = """ - query defer_query { - dog { - ... @defer(label: 1) { - name - } - } - } - """ when: def validationErrors = validate(query) then: !validationErrors.isEmpty() validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'label' with value 'IntValue{value=1}' is not a valid 'String' - Expected an AST type of 'StringValue' but it was a 'IntValue'" - } + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + validationErrors.get(0).message == "Validation error (MisplacedDirective@[dog]) : Directive 'defer' is not allowed on operation 'subscription'" + } - def "Defer directive is allowed on query root level"() { + def "Allows defer on mutation when it is not on root level"() { given: def query = """ - query defer_query { - ... @defer { + mutation pets { dog { - name - } + ... @defer { + name + } + } } - } """ when: - def validationErrors = validate(query) + traverse(query) then: - validationErrors.isEmpty() + errorCollector.errors.isEmpty() } static List validate(String query) { From df64da820adc860d71db1ffb59f804aa627ee96d Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 7 Feb 2024 13:33:57 +1000 Subject: [PATCH 142/393] wip --- .../java/graphql/agent/GraphQLJavaAgent.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 178f23186f..286b38a914 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -10,8 +10,10 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.implementation.bytecode.assign.Assigner; import org.dataloader.DataLoader; +import org.dataloader.DataLoaderRegistry; import java.lang.instrument.Instrumentation; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -113,12 +115,10 @@ public static void load(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object da class DataLoaderRegistryAdvice { @Advice.OnMethodEnter - public static void dispatchAll() { - // StackWalker instance = StackWalker.getInstance(); - // instance.forEach(stackFrame -> { - // System.out.println(stackFrame.getClassName() + " " + stackFrame.getMethodName() + " " + stackFrame.getLineNumber()); - // }); - System.out.println("calling dispatchAll"); + public static void dispatchAll(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoaderRegistry) { + List> dataLoaders = ((DataLoaderRegistry) dataLoaderRegistry).getDataLoaders(); + ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoaders.get(0)); + System.out.println("calling dispatchAll for " + executionId); } } @@ -134,7 +134,13 @@ public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext ex @Advice.OnMethodExit public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext) { - System.out.println("execution finished for: " + executionContext.getExecutionId()); + ExecutionId executionId = executionContext.getExecutionId(); + System.out.println("execution finished for: " + executionId); + // cleanup + GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { + GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); + }); + GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); } } From 921a171a96e7b5ed70e606f4bf51306dcc4cbd70 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:36:19 +1100 Subject: [PATCH 143/393] Use property instead --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6c61a0c167..fa86d12b08 100644 --- a/build.gradle +++ b/build.gradle @@ -348,7 +348,7 @@ nexusPublishing { } signing { - required { !version.endsWith('-SNAPSHOT') } + required { !project.hasProperty('publishToMavenLocal') } def signingKey = System.env.MAVEN_CENTRAL_PGP_KEY useInMemoryPgpKeys(signingKey, "") sign publishing.publications From 2627083cf99a9e4c2f79137f50f7d15062fb5a3f Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 7 Feb 2024 13:57:22 +1000 Subject: [PATCH 144/393] fix --- agent/src/main/java/graphql/agent/GraphQLJavaAgent.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 286b38a914..4167a93647 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -137,10 +137,10 @@ public static void executeOperationExit(@Advice.Argument(0) ExecutionContext exe ExecutionId executionId = executionContext.getExecutionId(); System.out.println("execution finished for: " + executionId); // cleanup - GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { - GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); - }); - GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); + // GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { + // GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); + // }); + // GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); } } From b37033b590720fb975f765f229f36de28c5c516d Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 7 Feb 2024 16:17:59 +1000 Subject: [PATCH 145/393] wip --- .../src/test/java/graphql/test/AgentTest.java | 22 ++- .../src/test/java/graphql/test/TestQuery.java | 5 +- .../java/graphql/agent/GraphQLJavaAgent.java | 161 +++++++++++++----- 3 files changed, 135 insertions(+), 53 deletions(-) diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java index 20a4057a0d..669ede98bd 100644 --- a/agent-test/src/test/java/graphql/test/AgentTest.java +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -5,8 +5,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; - public class AgentTest { @BeforeAll @@ -21,20 +19,20 @@ static void cleanup() { @Test void test() { ExecutionTrackingResult executionTrackingResult = TestQuery.executeQuery(); - assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(5); - assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); - assertThat(executionTrackingResult.getDfResultTypes("/issues")) - .isEqualTo(ExecutionTrackingResult.DFResultType.DONE_OK); + // assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(5); + // assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); + // assertThat(executionTrackingResult.getDfResultTypes("/issues")) + // .isEqualTo(ExecutionTrackingResult.DFResultType.DONE_OK); } @Test void testBatchLoader() { ExecutionTrackingResult executionTrackingResult = TestQuery.executeBatchedQuery(); - assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(9); - assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); - assertThat(executionTrackingResult.getDfResultTypes("/issues[0]/author")) - .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); - assertThat(executionTrackingResult.getDfResultTypes("/issues[1]/author")) - .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); + // assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(9); + // assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); + // assertThat(executionTrackingResult.getDfResultTypes("/issues[0]/author")) + // .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); + // assertThat(executionTrackingResult.getDfResultTypes("/issues[1]/author")) + // .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); } } diff --git a/agent-test/src/test/java/graphql/test/TestQuery.java b/agent-test/src/test/java/graphql/test/TestQuery.java index 43eab5ebad..a518ff3eed 100644 --- a/agent-test/src/test/java/graphql/test/TestQuery.java +++ b/agent-test/src/test/java/graphql/test/TestQuery.java @@ -47,7 +47,6 @@ static ExecutionTrackingResult executeQuery() { } static ExecutionTrackingResult executeBatchedQuery() { - System.out.println("ClassLoader: " + TestQuery.class.getClassLoader()); String sdl = "type Query{issues: [Issue]} " + "type Issue {id: ID, author: User}" + "type User {id: ID, name: String}"; @@ -57,7 +56,7 @@ static ExecutionTrackingResult executeBatchedQuery() { Map.of("id", "2", "title", "issue-2", "authorId", "user-2")); BatchLoader userBatchLoader = keys -> { - System.out.println("batch users with keys: " + keys); + // System.out.println("batch users with keys: " + keys); return CompletableFuture.supplyAsync(() -> { try { Thread.sleep(100); @@ -75,7 +74,7 @@ static ExecutionTrackingResult executeBatchedQuery() { DataFetcher> authorDF = (env) -> { DataLoader userLoader = env.getDataLoader("userLoader"); - System.out.println("author id: " + (String) ((Map) env.getSource()).get("authorId")); + // System.out.println("author id: " + (String) ((Map) env.getSource()).get("authorId")); return userLoader.load((String) ((Map) env.getSource()).get("authorId")); }; TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(sdl); diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 4167a93647..1a8bf9cf2a 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -11,14 +11,19 @@ import net.bytebuddy.implementation.bytecode.assign.Assigner; import org.dataloader.DataLoader; import org.dataloader.DataLoaderRegistry; +import org.dataloader.DispatchResult; import java.lang.instrument.Instrumentation; +import java.lang.reflect.Field; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_CANCELLED; +import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_EXCEPTIONALLY; +import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_OK; +import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.PENDING; import static graphql.agent.result.ExecutionTrackingResult.EXECUTION_TRACKING_KEY; import static net.bytebuddy.matcher.ElementMatchers.nameMatches; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -26,21 +31,66 @@ public class GraphQLJavaAgent { public static class ExecutionData { - public Set resultPaths = ConcurrentHashMap.newKeySet(); - - public Map resultPathToDataLoader = new ConcurrentHashMap<>(); + public Map resultPathToDataLoaderUsed = new ConcurrentHashMap<>(); public Map dataLoaderToName = new ConcurrentHashMap<>(); + private final Map timePerPath = new ConcurrentHashMap<>(); + private final Map dfResultTypes = new ConcurrentHashMap<>(); + @Override public String toString() { return "ExecutionData{" + - "resultPathToDataLoader=" + resultPathToDataLoader + - ", dataLoaderToName=" + dataLoaderToName + + "resultPathToDataLoaderUsed=" + resultPathToDataLoaderUsed + + ", dataLoaderNames=" + dataLoaderToName.values() + + ", timePerPath=" + timePerPath + + ", dfResultTypes=" + dfResultTypes + '}'; } + + public enum DFResultType { + DONE_OK, + DONE_EXCEPTIONALLY, + DONE_CANCELLED, + PENDING, + } + + + public void start(ResultPath path, long startTime) { + timePerPath.put(path, startTime); + } + + public void end(ResultPath path, long endTime) { + timePerPath.put(path, endTime - timePerPath.get(path)); + } + + public int dataFetcherCount() { + return timePerPath.size(); + } + + public long getTime(ResultPath path) { + return timePerPath.get(path); + } + + public long getTime(String path) { + return timePerPath.get(ResultPath.parse(path)); + } + + public void setDfResultTypes(ResultPath resultPath, DFResultType resultTypes) { + dfResultTypes.put(resultPath, resultTypes); + } + + public DFResultType getDfResultTypes(ResultPath resultPath) { + return dfResultTypes.get(resultPath); + } + + public DFResultType getDfResultTypes(String resultPath) { + return dfResultTypes.get(ResultPath.parse(resultPath)); + } + + } - public static final Map executionDataMap = new ConcurrentHashMap<>(); + public static final Map executionIdToData = new ConcurrentHashMap<>(); public static final Map dataLoaderToExecutionId = new ConcurrentHashMap<>(); public static void agentmain(String agentArgs, Instrumentation inst) { @@ -48,32 +98,37 @@ public static void agentmain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() .type(named("graphql.execution.Execution")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - System.out.println("transforming " + typeDescription); + // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); }) .type(named("graphql.execution.ExecutionStrategy")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - System.out.println("Transforming " + typeDescription); return builder .visit(Advice.to(DataFetcherInvokeAdvice.class).on(nameMatches("invokeDataFetcher"))); }) .type(named("org.dataloader.DataLoaderRegistry")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - System.out.println("transforming " + typeDescription); + // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(DataLoaderRegistryAdvice.class).on(nameMatches("dispatchAll"))); }) .type(named("org.dataloader.DataLoader")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - System.out.println("transforming " + typeDescription); + // System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(DataLoaderLoadAdvice.class).on(nameMatches("load"))); + }) + .type(named("org.dataloader.DataLoaderHelper")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + // System.out.println("transforming " + typeDescription); return builder - .visit(Advice.to(DataLoaderAdvice.class).on(nameMatches("load"))); + .visit(Advice.to(DataLoaderHelperDispatchAdvice.class).on(nameMatches("dispatch"))); }) .type(named("graphql.schema.DataFetchingEnvironmentImpl")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - System.out.println("transforming " + typeDescription); + // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(DataFetchingEnvironmentAdvice.class).on(nameMatches("getDataLoader"))); }) @@ -89,25 +144,47 @@ class DataFetchingEnvironmentAdvice { public static void getDataLoader(@Advice.Argument(0) String dataLoaderName, @Advice.This(typing = Assigner.Typing.DYNAMIC) DataFetchingEnvironment dataFetchingEnvironment, @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) DataLoader dataLoader) { - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionDataMap.get(dataFetchingEnvironment.getExecutionId()); - System.out.println("execution data: " + executionData); - executionData.resultPathToDataLoader.put(dataFetchingEnvironment.getExecutionStepInfo().getPath(), dataLoader); - executionData.dataLoaderToName.put(dataLoader, dataLoaderName); - GraphQLJavaAgent.dataLoaderToExecutionId.put(dataLoader, dataFetchingEnvironment.getExecutionId()); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(dataFetchingEnvironment.getExecutionId()); + // System.out.println("execution data: " + executionData); + ResultPath resultPath = dataFetchingEnvironment.getExecutionStepInfo().getPath(); + executionData.resultPathToDataLoaderUsed.put(resultPath, dataLoaderName); - System.out.println(dataLoaderName + " > " + dataLoader); + // System.out.println(dataLoaderName + " > " + dataLoader); } } +class DataLoaderHelperDispatchAdvice { -class DataLoaderAdvice { + @Advice.OnMethodExit + public static void dispatch(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoaderHelper, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) DispatchResult dispatchResult) { + try { + // System.out.println("dataloader helper Dispatch " + dataLoaderHelper + " load for execution " + dispatchResult); + Field field = dataLoaderHelper.getClass().getDeclaredField("dataLoader"); + field.setAccessible(true); + DataLoader dataLoader = (DataLoader) field.get(dataLoaderHelper); + // System.out.println("dataLoader: " + dataLoader); + ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); + String dataLoaderName = GraphQLJavaAgent.executionIdToData.get(executionId).dataLoaderToName.get(dataLoader); + + System.out.println("dataloader " + dataLoaderName + " dispatch result size:" + dispatchResult.getKeysCount()); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + +} + +class DataLoaderLoadAdvice { @Advice.OnMethodEnter public static void load(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoader) { ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); - String dataLoaderName = GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.get(dataLoader); - System.out.println("dataloader " + dataLoaderName + " load for execution " + executionId); + String dataLoaderName = GraphQLJavaAgent.executionIdToData.get(executionId).dataLoaderToName.get(dataLoader); + // System.out.println("dataloader " + dataLoaderName + " load for execution " + executionId); } } @@ -129,18 +206,26 @@ class ExecutionAdvice { public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { System.out.println("execution started for: " + executionContext.getExecutionId()); executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); - GraphQLJavaAgent.executionDataMap.put(executionContext.getExecutionId(), new GraphQLJavaAgent.ExecutionData()); + GraphQLJavaAgent.ExecutionData executionData = new GraphQLJavaAgent.ExecutionData(); + GraphQLJavaAgent.executionIdToData.put(executionContext.getExecutionId(), executionData); + + DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); + for (String name : dataLoaderRegistry.getDataLoadersMap().keySet()) { + DataLoader dataLoader = dataLoaderRegistry.getDataLoader(name); + GraphQLJavaAgent.dataLoaderToExecutionId.put(dataLoader, executionContext.getExecutionId()); + executionData.dataLoaderToName.put(dataLoader, name); + } } @Advice.OnMethodExit public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext) { ExecutionId executionId = executionContext.getExecutionId(); - System.out.println("execution finished for: " + executionId); - // cleanup - // GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { - // GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); - // }); - // GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); + System.out.println("execution finished for: " + executionId + " with data " + GraphQLJavaAgent.executionIdToData.get(executionId)); + // cleanup + // GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { + // GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); + // }); + // GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); } } @@ -148,28 +233,28 @@ class DataFetcherInvokeAdvice { @Advice.OnMethodEnter public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext executionContext, @Advice.Argument(1) ExecutionStrategyParameters parameters) { - - ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); - executionTrackingResult.start(parameters.getPath(), System.nanoTime()); + GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()) + .start(parameters.getPath(), System.nanoTime()); } @Advice.OnMethodExit public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext executionContext, @Advice.Argument(1) ExecutionStrategyParameters parameters, @Advice.Return CompletableFuture result) { - ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); + // ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); ResultPath path = parameters.getPath(); - executionTrackingResult.end(path, System.nanoTime()); + executionData.end(path, System.nanoTime()); if (result.isDone()) { if (result.isCancelled()) { - executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.DONE_CANCELLED); + executionData.setDfResultTypes(path, DONE_CANCELLED); } else if (result.isCompletedExceptionally()) { - executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.DONE_EXCEPTIONALLY); + executionData.setDfResultTypes(path, DONE_EXCEPTIONALLY); } else { - executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.DONE_OK); + executionData.setDfResultTypes(path, DONE_OK); } } else { - executionTrackingResult.setDfResultTypes(path, ExecutionTrackingResult.DFResultType.PENDING); + executionData.setDfResultTypes(path, PENDING); } } From a13fe7bf54c5b0aa6a0c75835d5c79a21ac6b13d Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 7 Feb 2024 16:49:17 +1000 Subject: [PATCH 146/393] wip --- .../java/graphql/agent/GraphQLJavaAgent.java | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 1a8bf9cf2a..5156b9c02a 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -15,10 +15,12 @@ import java.lang.instrument.Instrumentation; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_CANCELLED; import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_EXCEPTIONALLY; @@ -31,12 +33,55 @@ public class GraphQLJavaAgent { public static class ExecutionData { - public Map resultPathToDataLoaderUsed = new ConcurrentHashMap<>(); - public Map dataLoaderToName = new ConcurrentHashMap<>(); + public final AtomicLong startExecutionTime = new AtomicLong(); + public final AtomicLong endExecutionTime = new AtomicLong(); + public final Map resultPathToDataLoaderUsed = new ConcurrentHashMap<>(); + public final Map dataLoaderToName = new ConcurrentHashMap<>(); private final Map timePerPath = new ConcurrentHashMap<>(); private final Map dfResultTypes = new ConcurrentHashMap<>(); + public static class BatchLoadingCall { + public BatchLoadingCall(int resultCount) { + this.resultCount = resultCount; + } + + public int resultCount; + } + + public final Map> dataLoaderNameToBatchCall = new ConcurrentHashMap<>(); + + public String print(String executionId) { + StringBuilder s = new StringBuilder(); + s.append("==========================").append("\n"); + s.append("Summary for execution with id ").append(executionId).append("\n"); + s.append("==========================").append("\n"); + s.append("Execution time in ms:").append((endExecutionTime.get() - startExecutionTime.get()) / 1_000_000L).append("\n"); + s.append("Fields count: ").append(timePerPath.keySet().size()).append("\n"); + s.append("Blocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType != PENDING).count()).append("\n"); + s.append("Nonblocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType == PENDING).count()).append("\n"); + s.append("DataLoaders used: ").append(dataLoaderToName.size()).append("\n"); + s.append("DataLoader names: ").append(dataLoaderToName.values()).append("\n"); + s.append("BatchLoader calls details: ").append("\n"); + s.append("==========================").append("\n"); + for (String dataLoaderName : dataLoaderNameToBatchCall.keySet()) { + s.append("DataLoader: '").append(dataLoaderName).append("' called ").append(dataLoaderNameToBatchCall.get(dataLoaderName).size()).append(" times, ").append("\n"); + for (BatchLoadingCall batchLoadingCall : dataLoaderNameToBatchCall.get(dataLoaderName)) { + s.append("Batch call with ").append(batchLoadingCall.resultCount).append(" results").append("\n"); + } + } + s.append("Field details:").append("\n"); + s.append("===============").append("\n"); + for (ResultPath path : timePerPath.keySet()) { + s.append("Field: '").append(path).append("' took: ").append(timePerPath.get(path)).append(" nano seconds, ").append("\n"); + s.append("Result type: ").append(dfResultTypes.get(path)).append("\n"); + } + s.append("==========================").append("\n"); + s.append("==========================").append("\n"); + return s.toString(); + + } + @Override public String toString() { return "ExecutionData{" + @@ -166,9 +211,11 @@ public static void dispatch(@Advice.This(typing = Assigner.Typing.DYNAMIC) Objec DataLoader dataLoader = (DataLoader) field.get(dataLoaderHelper); // System.out.println("dataLoader: " + dataLoader); ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); - String dataLoaderName = GraphQLJavaAgent.executionIdToData.get(executionId).dataLoaderToName.get(dataLoader); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + String dataLoaderName = executionData.dataLoaderToName.get(dataLoader); - System.out.println("dataloader " + dataLoaderName + " dispatch result size:" + dispatchResult.getKeysCount()); + executionData.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); + executionData.dataLoaderNameToBatchCall.get(dataLoaderName).add(new GraphQLJavaAgent.ExecutionData.BatchLoadingCall(dispatchResult.getKeysCount())); } catch (Exception e) { e.printStackTrace(); @@ -204,9 +251,11 @@ class ExecutionAdvice { @Advice.OnMethodEnter public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { + GraphQLJavaAgent.ExecutionData executionData = new GraphQLJavaAgent.ExecutionData(); + executionData.startExecutionTime.set(System.nanoTime()); System.out.println("execution started for: " + executionContext.getExecutionId()); executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); - GraphQLJavaAgent.ExecutionData executionData = new GraphQLJavaAgent.ExecutionData(); + GraphQLJavaAgent.executionIdToData.put(executionContext.getExecutionId(), executionData); DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); @@ -220,12 +269,15 @@ public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext ex @Advice.OnMethodExit public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext) { ExecutionId executionId = executionContext.getExecutionId(); - System.out.println("execution finished for: " + executionId + " with data " + GraphQLJavaAgent.executionIdToData.get(executionId)); - // cleanup - // GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { - // GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); - // }); - // GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + executionData.endExecutionTime.set(System.nanoTime()); + System.out.println("execution finished for: " + executionId + " with data " + executionData); + System.out.println(executionData.print(executionId.toString())); + // cleanup + // GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { + // GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); + // }); + // GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); } } From c9605413c1375e3befc59b4afb352b67e362cdb2 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 7 Feb 2024 16:53:07 +1000 Subject: [PATCH 147/393] wip --- agent/src/main/java/graphql/agent/GraphQLJavaAgent.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 5156b9c02a..02307aa0f8 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -66,6 +66,13 @@ public String print(String executionId) { s.append("==========================").append("\n"); for (String dataLoaderName : dataLoaderNameToBatchCall.keySet()) { s.append("DataLoader: '").append(dataLoaderName).append("' called ").append(dataLoaderNameToBatchCall.get(dataLoaderName).size()).append(" times, ").append("\n"); + List resultPathUsed = new ArrayList<>(); + for (ResultPath resultPath : resultPathToDataLoaderUsed.keySet()) { + if (resultPathToDataLoaderUsed.get(resultPath).equals(dataLoaderName)) { + resultPathUsed.add(resultPath); + } + } + s.append("DataLoader: '").append(dataLoaderName).append("' used in fields: ").append(resultPathUsed).append("\n"); for (BatchLoadingCall batchLoadingCall : dataLoaderNameToBatchCall.get(dataLoaderName)) { s.append("Batch call with ").append(batchLoadingCall.resultCount).append(" results").append("\n"); } From c6d830c36775edb67e4cdc3ef3848d591f3d4d45 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 8 Feb 2024 06:42:57 +1100 Subject: [PATCH 148/393] Undo renaming to NormalizedDeferExecution: will be done in another PR --- .../incremental/DeferredExecution.java | 3 +- .../normalized/ExecutableNormalizedField.java | 18 +++++----- .../ExecutableNormalizedOperationFactory.java | 36 +++++++++---------- ...tableNormalizedOperationToAstCompiler.java | 8 ++--- ...eferExecution.java => DeferExecution.java} | 4 +-- 5 files changed, 35 insertions(+), 34 deletions(-) rename src/main/java/graphql/normalized/incremental/{NormalizedDeferExecution.java => DeferExecution.java} (96%) diff --git a/src/main/java/graphql/execution/incremental/DeferredExecution.java b/src/main/java/graphql/execution/incremental/DeferredExecution.java index 497823a285..b8560b8535 100644 --- a/src/main/java/graphql/execution/incremental/DeferredExecution.java +++ b/src/main/java/graphql/execution/incremental/DeferredExecution.java @@ -1,13 +1,14 @@ package graphql.execution.incremental; import graphql.ExperimentalApi; +import graphql.normalized.incremental.DeferExecution; import javax.annotation.Nullable; /** * Represents details about the defer execution that can be associated with a {@link graphql.execution.MergedField}. *

- * This representation is used during graphql execution. Check {@link graphql.normalized.incremental.NormalizedDeferExecution} + * This representation is used during graphql execution. Check {@link DeferExecution} * for the normalized representation of @defer. */ @ExperimentalApi diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index bc2eede068..3a8d36af08 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -10,7 +10,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.NormalizedDeferExecution; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -66,7 +66,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -262,12 +262,12 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } - public void addDeferExecutions(Collection deferExecutions) { + public void addDeferExecutions(Collection deferExecutions) { this.deferExecutions.addAll(deferExecutions); } @@ -477,11 +477,11 @@ public ExecutableNormalizedField getParent() { } /** - * @return the {@link NormalizedDeferExecution}s associated with this {@link ExecutableNormalizedField}. - * @see NormalizedDeferExecution + * @return the {@link DeferExecution}s associated with this {@link ExecutableNormalizedField}. + * @see DeferExecution */ @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -612,7 +612,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -683,7 +683,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 39b35bd501..8ce24c8205 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -30,7 +30,7 @@ import graphql.language.Selection; import graphql.language.SelectionSet; import graphql.language.VariableDefinition; -import graphql.normalized.incremental.NormalizedDeferExecution; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -631,12 +631,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - NormalizedDeferExecution collectedDeferExecution = collectedField.deferExecution; + DeferExecution collectedDeferExecution = collectedField.deferExecution; if (collectedDeferExecution != null) { deferExecutionsBuilder.add(collectedDeferExecution); @@ -644,7 +644,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build(); + Set deferExecutions = deferExecutionsBuilder.build(); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -662,7 +662,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - Set filteredDeferExecutions = deferExecutions.stream() + Set filteredDeferExecutions = deferExecutions.stream() .filter(filterExecutionsFromType(objectType)) .collect(toCollection(LinkedHashSet::new)); @@ -671,7 +671,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { + private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { String objectTypeName = objectType.getName(); return deferExecution -> deferExecution.getPossibleTypes() .stream() @@ -679,9 +679,9 @@ private static Predicate filterExecutionsFromType(Grap .anyMatch(objectTypeName::equals); } - private Set listDuplicatedLabels(Collection deferExecutions) { + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() - .map(NormalizedDeferExecution::getLabel) + .map(DeferExecution::getLabel) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() @@ -695,7 +695,7 @@ private void collectFromSelectionSet(SelectionSet selectionSet, List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - NormalizedDeferExecution deferExecution + DeferExecution deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { @@ -729,7 +729,7 @@ private void collectFragmentSpread(List result, GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(this.graphQLSchema.getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition); - NormalizedDeferExecution newDeferExecution = buildDeferExecution( + DeferExecution newDeferExecution = buildDeferExecution( fragmentSpread.getDirectives(), newPossibleObjects); @@ -753,7 +753,7 @@ private void collectInlineFragment(List result, } - NormalizedDeferExecution newDeferExecution = buildDeferExecution( + DeferExecution newDeferExecution = buildDeferExecution( inlineFragment.getDirectives(), newPossibleObjects ); @@ -761,7 +761,7 @@ private void collectInlineFragment(List result, collectFromSelectionSet(inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } - private NormalizedDeferExecution buildDeferExecution( + private DeferExecution buildDeferExecution( List directives, Set newPossibleObjects) { if(!options.deferSupport) { @@ -771,7 +771,7 @@ private NormalizedDeferExecution buildDeferExecution( return IncrementalUtils.createDeferredExecution( this.coercedVariableValues.toMap(), directives, - (label) -> new NormalizedDeferExecution(label, newPossibleObjects) + (label) -> new DeferExecution(label, newPossibleObjects) ); } @@ -779,7 +779,7 @@ private void collectField(List result, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - NormalizedDeferExecution deferExecution + DeferExecution deferExecution ) { if (!conditionalNodes.shouldInclude(field, this.coercedVariableValues.toMap(), @@ -846,9 +846,9 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; - NormalizedDeferExecution deferExecution; + DeferExecution deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, NormalizedDeferExecution deferExecution) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; @@ -879,9 +879,9 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { private static class CollectedFieldGroup { Set objectTypes; Set fields; - Set deferExecutions; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; this.deferExecutions = deferExecutions; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 22e1340f90..051d4655de 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,7 +23,7 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.NormalizedDeferExecution; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -277,7 +277,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -489,9 +489,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final NormalizedDeferExecution deferExecution; + private final DeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java similarity index 96% rename from src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java rename to src/main/java/graphql/normalized/incremental/DeferExecution.java index 82d87153c9..71488b03ca 100644 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -103,11 +103,11 @@ * */ @ExperimentalApi -public class NormalizedDeferExecution { +public class DeferExecution { private final String label; private final Set possibleTypes; - public NormalizedDeferExecution(@Nullable String label, Set possibleTypes) { + public DeferExecution(@Nullable String label, Set possibleTypes) { this.label = label; this.possibleTypes = possibleTypes; } From fe710a99fc5e0c19123c0b9510b421b72ee425c4 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 8 Feb 2024 06:50:14 +1100 Subject: [PATCH 149/393] Undo refactoring around IncrementalNodes: will be done on future PR --- .../ExecutableNormalizedOperationFactory.java | 12 +++- .../incremental/IncrementalNodes.java | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/main/java/graphql/normalized/incremental/IncrementalNodes.java diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 8ce24c8205..9367c1d58d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -17,7 +17,6 @@ import graphql.execution.conditional.ConditionalNodes; import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; -import graphql.execution.incremental.IncrementalUtils; import graphql.introspection.Introspection; import graphql.language.Directive; import graphql.language.Document; @@ -29,8 +28,10 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; +import graphql.language.TypeName; import graphql.language.VariableDefinition; import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -190,6 +191,7 @@ public boolean getDeferSupport() { } private static final ConditionalNodes conditionalNodes = new ConditionalNodes(); + private static final IncrementalNodes incrementalNodes = new IncrementalNodes(); private ExecutableNormalizedOperationFactory() { @@ -731,6 +733,7 @@ private void collectFragmentSpread(List result, DeferExecution newDeferExecution = buildDeferExecution( fragmentSpread.getDirectives(), + fragmentDefinition.getTypeCondition(), newPossibleObjects); collectFromSelectionSet(fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); @@ -755,6 +758,7 @@ private void collectInlineFragment(List result, DeferExecution newDeferExecution = buildDeferExecution( inlineFragment.getDirectives(), + inlineFragment.getTypeCondition(), newPossibleObjects ); @@ -763,15 +767,17 @@ private void collectInlineFragment(List result, private DeferExecution buildDeferExecution( List directives, + TypeName typeCondition, Set newPossibleObjects) { if(!options.deferSupport) { return null; } - return IncrementalUtils.createDeferredExecution( + return incrementalNodes.createDeferExecution( this.coercedVariableValues.toMap(), directives, - (label) -> new DeferExecution(label, newPossibleObjects) + typeCondition, + newPossibleObjects ); } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java new file mode 100644 index 0000000000..025b9333a0 --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -0,0 +1,55 @@ +package graphql.normalized.incremental; + +import graphql.Assert; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.CoercedVariables; +import graphql.execution.ValuesResolver; +import graphql.language.Directive; +import graphql.language.NodeUtil; +import graphql.language.TypeName; +import graphql.schema.GraphQLObjectType; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static graphql.Directives.DeferDirective; + +@Internal +public class IncrementalNodes { + + public DeferExecution createDeferExecution( + Map variables, + List directives, + @Nullable TypeName targetType, + Set possibleTypes + ) { + Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); + + if (deferDirective != null) { + Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); + + Object flag = argumentValues.get("if"); + Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + if (!((Boolean) flag)) { + return null; + } + + Object label = argumentValues.get("label"); + + if (label == null) { + return new DeferExecution(null, possibleTypes); + } + + Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); + + return new DeferExecution((String) label, possibleTypes); + } + + return null; + } +} From 61ca8d1c7b76a5d2d167a22bef139f8a82796242 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 8 Feb 2024 07:00:06 +1100 Subject: [PATCH 150/393] Refactoring method to receive individual parameters instead of object --- src/main/java/graphql/execution/ExecutionStrategy.java | 6 +++++- .../graphql/execution/incremental/DeferredCallContext.java | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index aa6f52c77c..05297411cf 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -365,7 +365,11 @@ protected CompletableFuture handleFetchingException( .exception(e) .build(); - parameters.getDeferredCallContext().onFetchingException(parameters, e); + parameters.getDeferredCallContext().onFetchingException( + parameters.getPath(), + parameters.getField().getSingleField().getSourceLocation(), + e + ); try { return asyncHandleException(dataFetcherExceptionHandler, handlerParameters); diff --git a/src/main/java/graphql/execution/incremental/DeferredCallContext.java b/src/main/java/graphql/execution/incremental/DeferredCallContext.java index 0c12f426b1..9596799ba2 100644 --- a/src/main/java/graphql/execution/incremental/DeferredCallContext.java +++ b/src/main/java/graphql/execution/incremental/DeferredCallContext.java @@ -3,7 +3,8 @@ import graphql.ExceptionWhileDataFetching; import graphql.GraphQLError; import graphql.Internal; -import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.ResultPath; +import graphql.language.SourceLocation; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -22,8 +23,8 @@ public class DeferredCallContext { private final List errors = new CopyOnWriteArrayList<>(); - public void onFetchingException(ExecutionStrategyParameters parameters, Throwable e) { - ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(parameters.getPath(), e, parameters.getField().getSingleField().getSourceLocation()); + public void onFetchingException(ResultPath path, SourceLocation sourceLocation, Throwable throwable) { + ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, throwable, sourceLocation); onError(error); } From 864bf2e8a3c255732e33a86c1b766fc49cb92edb Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 8 Feb 2024 07:05:56 +1100 Subject: [PATCH 151/393] Replace synchronized with runBlocked --- .../dataloader/FieldLevelTrackingApproach.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index cc6537158c..1773683084 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -186,9 +186,10 @@ InstrumentationContext beginDeferredField(InstrumentationDeferr // Create a new CallStack, since every deferred fields can execute in parallel. CallStack callStack = new CallStack(); int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); - synchronized (callStack) { - callStack.clearAndMarkCurrentLevelAsReady(level); - } + + callStack.lock.runLocked(() -> + callStack.clearAndMarkCurrentLevelAsReady(level) + ); return new InstrumentationContext<>() { @Override From c8b0f31cc25edf1e980fd4232774e289033892ac Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 8 Feb 2024 11:18:20 +1000 Subject: [PATCH 152/393] wip --- .../graphql/agent/AfterExecutionHandler.java | 25 +++++++++++++++++++ .../java/graphql/agent/GraphQLJavaAgent.java | 24 ++++++++++++------ 2 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 agent/src/main/java/graphql/agent/AfterExecutionHandler.java diff --git a/agent/src/main/java/graphql/agent/AfterExecutionHandler.java b/agent/src/main/java/graphql/agent/AfterExecutionHandler.java new file mode 100644 index 0000000000..9e44f6f8b5 --- /dev/null +++ b/agent/src/main/java/graphql/agent/AfterExecutionHandler.java @@ -0,0 +1,25 @@ +package graphql.agent; + +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionId; + +import java.util.function.BiConsumer; + +public class AfterExecutionHandler implements BiConsumer { + + private final ExecutionContext executionContext; + + public AfterExecutionHandler(ExecutionContext executionContext) { + this.executionContext = executionContext; + } + + public void accept(Object o, Throwable throwable) { + ExecutionId executionId = executionContext.getExecutionId(); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + executionData.endExecutionTime.set(System.nanoTime()); + System.out.println("execution finished for: " + executionId + " with data " + executionData); + System.out.println(executionData.print(executionId.toString())); + } + + +} diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 02307aa0f8..e557514c69 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_CANCELLED; import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_EXCEPTIONALLY; @@ -33,12 +34,15 @@ public class GraphQLJavaAgent { public static class ExecutionData { + public final AtomicReference startThread = new AtomicReference<>(); + public final AtomicReference endThread = new AtomicReference<>(); public final AtomicLong startExecutionTime = new AtomicLong(); public final AtomicLong endExecutionTime = new AtomicLong(); public final Map resultPathToDataLoaderUsed = new ConcurrentHashMap<>(); public final Map dataLoaderToName = new ConcurrentHashMap<>(); private final Map timePerPath = new ConcurrentHashMap<>(); + private final Map invocationThreadPerPath = new ConcurrentHashMap<>(); private final Map dfResultTypes = new ConcurrentHashMap<>(); public static class BatchLoadingCall { @@ -150,6 +154,7 @@ public static void agentmain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() .type(named("graphql.execution.Execution")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + // ClassInjector.UsingInstrumentation.of() // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); @@ -184,6 +189,7 @@ public static void agentmain(String agentArgs, Instrumentation inst) { return builder .visit(Advice.to(DataFetchingEnvironmentAdvice.class).on(nameMatches("getDataLoader"))); }) + .disableClassFormatChanges() .installOn(inst); } @@ -254,12 +260,13 @@ public static void dispatchAll(@Advice.This(typing = Assigner.Typing.DYNAMIC) Ob } -class ExecutionAdvice { +class ExecutionAdvice{ @Advice.OnMethodEnter public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { GraphQLJavaAgent.ExecutionData executionData = new GraphQLJavaAgent.ExecutionData(); executionData.startExecutionTime.set(System.nanoTime()); + executionData.startThread.set(Thread.currentThread().getName()); System.out.println("execution started for: " + executionContext.getExecutionId()); executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); @@ -274,12 +281,15 @@ public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext ex } @Advice.OnMethodExit - public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext) { - ExecutionId executionId = executionContext.getExecutionId(); - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); - executionData.endExecutionTime.set(System.nanoTime()); - System.out.println("execution finished for: " + executionId + " with data " + executionData); - System.out.println(executionData.print(executionId.toString())); + public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) CompletableFuture result) { + + result.whenComplete(new AfterExecutionHandler(executionContext)); + // ExecutionId executionId = executionContext.getExecutionId(); + // GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + // executionData.endExecutionTime.set(System.nanoTime()); + // System.out.println("execution finished for: " + executionId + " with data " + executionData); + // System.out.println(executionData.print(executionId.toString())); // cleanup // GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { // GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); From 1b4c2fb0220c9d14f2de3676ce807327823a3a97 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 8 Feb 2024 11:21:23 +1000 Subject: [PATCH 153/393] wip --- .../graphql/agent/AfterExecutionHandler.java | 25 ------ .../java/graphql/agent/GraphQLJavaAgent.java | 86 +++++++++++-------- 2 files changed, 49 insertions(+), 62 deletions(-) delete mode 100644 agent/src/main/java/graphql/agent/AfterExecutionHandler.java diff --git a/agent/src/main/java/graphql/agent/AfterExecutionHandler.java b/agent/src/main/java/graphql/agent/AfterExecutionHandler.java deleted file mode 100644 index 9e44f6f8b5..0000000000 --- a/agent/src/main/java/graphql/agent/AfterExecutionHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package graphql.agent; - -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionId; - -import java.util.function.BiConsumer; - -public class AfterExecutionHandler implements BiConsumer { - - private final ExecutionContext executionContext; - - public AfterExecutionHandler(ExecutionContext executionContext) { - this.executionContext = executionContext; - } - - public void accept(Object o, Throwable throwable) { - ExecutionId executionId = executionContext.getExecutionId(); - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); - executionData.endExecutionTime.set(System.nanoTime()); - System.out.println("execution finished for: " + executionId + " with data " + executionData); - System.out.println(executionData.print(executionId.toString())); - } - - -} diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index e557514c69..b8873247da 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -22,6 +22,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_CANCELLED; import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_EXCEPTIONALLY; @@ -193,6 +194,54 @@ public static void agentmain(String agentArgs, Instrumentation inst) { .installOn(inst); } + + public static class ExecutionAdvice { + + public static class AfterExecutionHandler implements BiConsumer { + + private final ExecutionContext executionContext; + + public AfterExecutionHandler(ExecutionContext executionContext) { + this.executionContext = executionContext; + } + + public void accept(Object o, Throwable throwable) { + ExecutionId executionId = executionContext.getExecutionId(); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + executionData.endExecutionTime.set(System.nanoTime()); + System.out.println("execution finished for: " + executionId + " with data " + executionData); + System.out.println(executionData.print(executionId.toString())); + } + + } + + + @Advice.OnMethodEnter + public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { + GraphQLJavaAgent.ExecutionData executionData = new GraphQLJavaAgent.ExecutionData(); + executionData.startExecutionTime.set(System.nanoTime()); + executionData.startThread.set(Thread.currentThread().getName()); + System.out.println("execution started for: " + executionContext.getExecutionId()); + executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); + + GraphQLJavaAgent.executionIdToData.put(executionContext.getExecutionId(), executionData); + + DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); + for (String name : dataLoaderRegistry.getDataLoadersMap().keySet()) { + DataLoader dataLoader = dataLoaderRegistry.getDataLoader(name); + GraphQLJavaAgent.dataLoaderToExecutionId.put(dataLoader, executionContext.getExecutionId()); + executionData.dataLoaderToName.put(dataLoader, name); + } + } + + @Advice.OnMethodExit + public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) CompletableFuture result) { + + result.whenComplete(new AfterExecutionHandler(executionContext)); + } + } + } class DataFetchingEnvironmentAdvice { @@ -260,43 +309,6 @@ public static void dispatchAll(@Advice.This(typing = Assigner.Typing.DYNAMIC) Ob } -class ExecutionAdvice{ - - @Advice.OnMethodEnter - public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { - GraphQLJavaAgent.ExecutionData executionData = new GraphQLJavaAgent.ExecutionData(); - executionData.startExecutionTime.set(System.nanoTime()); - executionData.startThread.set(Thread.currentThread().getName()); - System.out.println("execution started for: " + executionContext.getExecutionId()); - executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); - - GraphQLJavaAgent.executionIdToData.put(executionContext.getExecutionId(), executionData); - - DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); - for (String name : dataLoaderRegistry.getDataLoadersMap().keySet()) { - DataLoader dataLoader = dataLoaderRegistry.getDataLoader(name); - GraphQLJavaAgent.dataLoaderToExecutionId.put(dataLoader, executionContext.getExecutionId()); - executionData.dataLoaderToName.put(dataLoader, name); - } - } - - @Advice.OnMethodExit - public static void executeOperationExit(@Advice.Argument(0) ExecutionContext executionContext, - @Advice.Return(typing = Assigner.Typing.DYNAMIC) CompletableFuture result) { - - result.whenComplete(new AfterExecutionHandler(executionContext)); - // ExecutionId executionId = executionContext.getExecutionId(); - // GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); - // executionData.endExecutionTime.set(System.nanoTime()); - // System.out.println("execution finished for: " + executionId + " with data " + executionData); - // System.out.println(executionData.print(executionId.toString())); - // cleanup - // GraphQLJavaAgent.executionDataMap.get(executionId).dataLoaderToName.forEach((dataLoader, s) -> { - // GraphQLJavaAgent.dataLoaderToExecutionId.remove(dataLoader); - // }); - // GraphQLJavaAgent.executionDataMap.remove(executionContext.getExecutionId()); - } -} class DataFetcherInvokeAdvice { @Advice.OnMethodEnter From 12ad6bac3114753fc7cf238b435d0f3f4c491a88 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 8 Feb 2024 11:44:36 +1000 Subject: [PATCH 154/393] wip --- .../java/graphql/agent/GraphQLJavaAgent.java | 97 ++++++++++++------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index b8873247da..28ddd73d61 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -42,8 +42,10 @@ public static class ExecutionData { public final Map resultPathToDataLoaderUsed = new ConcurrentHashMap<>(); public final Map dataLoaderToName = new ConcurrentHashMap<>(); - private final Map timePerPath = new ConcurrentHashMap<>(); - private final Map invocationThreadPerPath = new ConcurrentHashMap<>(); + public final Map timePerPath = new ConcurrentHashMap<>(); + public final Map finishedTimePerPath = new ConcurrentHashMap<>(); + public final Map finishedThreadPerPath = new ConcurrentHashMap<>(); + public final Map invocationThreadPerPath = new ConcurrentHashMap<>(); private final Map dfResultTypes = new ConcurrentHashMap<>(); public static class BatchLoadingCall { @@ -51,7 +53,7 @@ public BatchLoadingCall(int resultCount) { this.resultCount = resultCount; } - public int resultCount; + public final int resultCount; } public final Map> dataLoaderNameToBatchCall = new ConcurrentHashMap<>(); @@ -67,6 +69,7 @@ public String print(String executionId) { s.append("Nonblocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType == PENDING).count()).append("\n"); s.append("DataLoaders used: ").append(dataLoaderToName.size()).append("\n"); s.append("DataLoader names: ").append(dataLoaderToName.values()).append("\n"); + s.append("start thread: '" + startThread.get() + "' end thread: '" + endThread.get()).append("'\n"); s.append("BatchLoader calls details: ").append("\n"); s.append("==========================").append("\n"); for (String dataLoaderName : dataLoaderNameToBatchCall.keySet()) { @@ -209,6 +212,7 @@ public void accept(Object o, Throwable throwable) { ExecutionId executionId = executionContext.getExecutionId(); GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); executionData.endExecutionTime.set(System.nanoTime()); + executionData.endThread.set(Thread.currentThread().getName()); System.out.println("execution finished for: " + executionId + " with data " + executionData); System.out.println(executionData.print(executionId.toString())); } @@ -242,6 +246,63 @@ public static void executeOperationExit(@Advice.Argument(0) ExecutionContext exe } } + public static class DataFetcherInvokeAdvice { + + public static class DataFetcherFinishedHandler implements BiConsumer { + + private final ExecutionContext executionContext; + private final ExecutionStrategyParameters parameters; + private final long startTime; + + public DataFetcherFinishedHandler(ExecutionContext executionContext, ExecutionStrategyParameters parameters, long startTime) { + this.executionContext = executionContext; + this.parameters = parameters; + this.startTime = startTime; + } + + @Override + public void accept(Object o, Throwable throwable) { + ExecutionId executionId = executionContext.getExecutionId(); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + ResultPath path = parameters.getPath(); + executionData.finishedTimePerPath.put(path, System.nanoTime() - startTime); + executionData.finishedThreadPerPath.put(path, Thread.currentThread().getName()); + } + } + + @Advice.OnMethodEnter + public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext executionContext, + @Advice.Argument(1) ExecutionStrategyParameters parameters) { + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); + executionData.start(parameters.getPath(), System.nanoTime()); + executionData.invocationThreadPerPath.put(parameters.getPath(), Thread.currentThread().getName()); + } + + @Advice.OnMethodExit + public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext executionContext, + @Advice.Argument(1) ExecutionStrategyParameters parameters, + @Advice.Return CompletableFuture result) { + // ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); + GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); + ResultPath path = parameters.getPath(); + long startTime = executionData.timePerPath.get(path); + executionData.end(path, System.nanoTime()); + if (result.isDone()) { + if (result.isCancelled()) { + executionData.setDfResultTypes(path, DONE_CANCELLED); + } else if (result.isCompletedExceptionally()) { + executionData.setDfResultTypes(path, DONE_EXCEPTIONALLY); + } else { + executionData.setDfResultTypes(path, DONE_OK); + } + } else { + executionData.setDfResultTypes(path, PENDING); + } + result.whenComplete(new DataFetcherFinishedHandler(executionContext, parameters, startTime)); + } + + } + } class DataFetchingEnvironmentAdvice { @@ -310,33 +371,3 @@ public static void dispatchAll(@Advice.This(typing = Assigner.Typing.DYNAMIC) Ob } -class DataFetcherInvokeAdvice { - @Advice.OnMethodEnter - public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext executionContext, - @Advice.Argument(1) ExecutionStrategyParameters parameters) { - GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()) - .start(parameters.getPath(), System.nanoTime()); - } - - @Advice.OnMethodExit - public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext executionContext, - @Advice.Argument(1) ExecutionStrategyParameters parameters, - @Advice.Return CompletableFuture result) { - // ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); - ResultPath path = parameters.getPath(); - executionData.end(path, System.nanoTime()); - if (result.isDone()) { - if (result.isCancelled()) { - executionData.setDfResultTypes(path, DONE_CANCELLED); - } else if (result.isCompletedExceptionally()) { - executionData.setDfResultTypes(path, DONE_EXCEPTIONALLY); - } else { - executionData.setDfResultTypes(path, DONE_OK); - } - } else { - executionData.setDfResultTypes(path, PENDING); - } - } - -} From f11aeed16dedbbf973d83587232a7a794a8267d0 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 8 Feb 2024 12:02:39 +1000 Subject: [PATCH 155/393] wip --- .../src/test/java/graphql/test/AgentTest.java | 3 +++ .../java/graphql/agent/GraphQLJavaAgent.java | 21 +++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java index 669ede98bd..dd90eed174 100644 --- a/agent-test/src/test/java/graphql/test/AgentTest.java +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -28,6 +28,9 @@ void test() { @Test void testBatchLoader() { ExecutionTrackingResult executionTrackingResult = TestQuery.executeBatchedQuery(); + TestQuery.executeBatchedQuery(); + TestQuery.executeBatchedQuery(); + TestQuery.executeBatchedQuery(); // assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(9); // assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); // assertThat(executionTrackingResult.getDfResultTypes("/issues[0]/author")) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 28ddd73d61..dda94bafc4 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -45,7 +45,7 @@ public static class ExecutionData { public final Map timePerPath = new ConcurrentHashMap<>(); public final Map finishedTimePerPath = new ConcurrentHashMap<>(); public final Map finishedThreadPerPath = new ConcurrentHashMap<>(); - public final Map invocationThreadPerPath = new ConcurrentHashMap<>(); + public final Map startInvocationThreadPerPath = new ConcurrentHashMap<>(); private final Map dfResultTypes = new ConcurrentHashMap<>(); public static class BatchLoadingCall { @@ -69,7 +69,8 @@ public String print(String executionId) { s.append("Nonblocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType == PENDING).count()).append("\n"); s.append("DataLoaders used: ").append(dataLoaderToName.size()).append("\n"); s.append("DataLoader names: ").append(dataLoaderToName.values()).append("\n"); - s.append("start thread: '" + startThread.get() + "' end thread: '" + endThread.get()).append("'\n"); + s.append("start execution thread: '").append(startThread.get()).append("'\n"); + s.append("end execution thread: '").append(endThread.get()).append("'\n"); s.append("BatchLoader calls details: ").append("\n"); s.append("==========================").append("\n"); for (String dataLoaderName : dataLoaderNameToBatchCall.keySet()) { @@ -88,8 +89,13 @@ public String print(String executionId) { s.append("Field details:").append("\n"); s.append("===============").append("\n"); for (ResultPath path : timePerPath.keySet()) { - s.append("Field: '").append(path).append("' took: ").append(timePerPath.get(path)).append(" nano seconds, ").append("\n"); + s.append("Field: '").append(path).append("'\n"); + s.append("invocation time: ").append(timePerPath.get(path)).append(" nano seconds, ").append("\n"); + s.append("completion time: ").append(finishedTimePerPath.get(path)).append(" nano seconds, ").append("\n"); s.append("Result type: ").append(dfResultTypes.get(path)).append("\n"); + s.append("invoked in thread: ").append(startInvocationThreadPerPath.get(path)).append("\n"); + s.append("finished in thread: ").append(finishedThreadPerPath.get(path)).append("\n"); + s.append("-------------\n"); } s.append("==========================").append("\n"); s.append("==========================").append("\n"); @@ -265,6 +271,7 @@ public void accept(Object o, Throwable throwable) { ExecutionId executionId = executionContext.getExecutionId(); GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); ResultPath path = parameters.getPath(); + System.out.println("finished " + path); executionData.finishedTimePerPath.put(path, System.nanoTime() - startTime); executionData.finishedThreadPerPath.put(path, Thread.currentThread().getName()); } @@ -275,13 +282,13 @@ public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext e @Advice.Argument(1) ExecutionStrategyParameters parameters) { GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); executionData.start(parameters.getPath(), System.nanoTime()); - executionData.invocationThreadPerPath.put(parameters.getPath(), Thread.currentThread().getName()); + executionData.startInvocationThreadPerPath.put(parameters.getPath(), Thread.currentThread().getName()); } @Advice.OnMethodExit public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext executionContext, @Advice.Argument(1) ExecutionStrategyParameters parameters, - @Advice.Return CompletableFuture result) { + @Advice.Return(readOnly = false) CompletableFuture result) { // ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); ResultPath path = parameters.getPath(); @@ -298,7 +305,9 @@ public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext ex } else { executionData.setDfResultTypes(path, PENDING); } - result.whenComplete(new DataFetcherFinishedHandler(executionContext, parameters, startTime)); + // overriding the result to make sure the finished handler is called first when the DF is finished + // otherwise it is a completion tree instead of chain + result = result.whenComplete(new DataFetcherFinishedHandler(executionContext, parameters, startTime)); } } From c2a3dba225f6f3708c372c58fef6a632b5782a9c Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Thu, 8 Feb 2024 13:25:56 +1100 Subject: [PATCH 156/393] Remove space how did it get there --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fa86d12b08..b0b2172c78 100644 --- a/build.gradle +++ b/build.gradle @@ -348,7 +348,7 @@ nexusPublishing { } signing { - required { !project.hasProperty('publishToMavenLocal') } + required { !project.hasProperty('publishToMavenLocal') } def signingKey = System.env.MAVEN_CENTRAL_PGP_KEY useInMemoryPgpKeys(signingKey, "") sign publishing.publications From 5fc2fc77e773b7724f82197c8eade52b171768d3 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 8 Feb 2024 19:26:37 +1000 Subject: [PATCH 157/393] wip --- .../java/graphql/agent/GraphQLJavaAgent.java | 106 ++++++++++++------ 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index dda94bafc4..a827a304b5 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -31,6 +31,7 @@ import static graphql.agent.result.ExecutionTrackingResult.EXECUTION_TRACKING_KEY; import static net.bytebuddy.matcher.ElementMatchers.nameMatches; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; public class GraphQLJavaAgent { @@ -49,11 +50,13 @@ public static class ExecutionData { private final Map dfResultTypes = new ConcurrentHashMap<>(); public static class BatchLoadingCall { - public BatchLoadingCall(int resultCount) { - this.resultCount = resultCount; + public BatchLoadingCall(int keyCount, String threadName) { + this.keyCount = keyCount; + this.threadName = threadName; } - public final int resultCount; + public final int keyCount; + public final String threadName; } public final Map> dataLoaderNameToBatchCall = new ConcurrentHashMap<>(); @@ -74,7 +77,10 @@ public String print(String executionId) { s.append("BatchLoader calls details: ").append("\n"); s.append("==========================").append("\n"); for (String dataLoaderName : dataLoaderNameToBatchCall.keySet()) { - s.append("DataLoader: '").append(dataLoaderName).append("' called ").append(dataLoaderNameToBatchCall.get(dataLoaderName).size()).append(" times, ").append("\n"); + s.append("Batch call: '").append(dataLoaderName).append("' made ").append(dataLoaderNameToBatchCall.get(dataLoaderName).size()).append(" times, ").append("\n"); + for (BatchLoadingCall batchLoadingCall : dataLoaderNameToBatchCall.get(dataLoaderName)) { + s.append("Batch call with ").append(batchLoadingCall.keyCount).append(" keys ").append(" in thread ").append(batchLoadingCall.threadName).append("\n"); + } List resultPathUsed = new ArrayList<>(); for (ResultPath resultPath : resultPathToDataLoaderUsed.keySet()) { if (resultPathToDataLoaderUsed.get(resultPath).equals(dataLoaderName)) { @@ -82,9 +88,6 @@ public String print(String executionId) { } } s.append("DataLoader: '").append(dataLoaderName).append("' used in fields: ").append(resultPathUsed).append("\n"); - for (BatchLoadingCall batchLoadingCall : dataLoaderNameToBatchCall.get(dataLoaderName)) { - s.append("Batch call with ").append(batchLoadingCall.resultCount).append(" results").append("\n"); - } } s.append("Field details:").append("\n"); s.append("===============").append("\n"); @@ -191,7 +194,9 @@ public static void agentmain(String agentArgs, Instrumentation inst) { .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { // System.out.println("transforming " + typeDescription); return builder - .visit(Advice.to(DataLoaderHelperDispatchAdvice.class).on(nameMatches("dispatch"))); + .visit(Advice.to(DataLoaderHelperDispatchAdvice.class).on(nameMatches("dispatch"))) + .visit(Advice.to(DataLoaderHelperInvokeBatchLoaderAdvice.class) + .on(nameMatches("invokeLoader").and(takesArguments(List.class, List.class)))); }) .type(named("graphql.schema.DataFetchingEnvironmentImpl")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { @@ -271,7 +276,6 @@ public void accept(Object o, Throwable throwable) { ExecutionId executionId = executionContext.getExecutionId(); GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); ResultPath path = parameters.getPath(); - System.out.println("finished " + path); executionData.finishedTimePerPath.put(path, System.nanoTime() - startTime); executionData.finishedThreadPerPath.put(path, Thread.currentThread().getName()); } @@ -312,6 +316,63 @@ public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext ex } + + public static class DataLoaderHelperInvokeBatchLoaderAdvice { + + @Advice.OnMethodEnter + public static void invokeLoader(@Advice.Argument(0) List keys, + @Advice.Argument(1) List keysContext, + @Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoaderHelper) { + DataLoader dataLoader = getDataLoaderForHelper(dataLoaderHelper); + ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); + ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + String dataLoaderName = executionData.dataLoaderToName.get(dataLoader); + + synchronized (executionData.dataLoaderNameToBatchCall) { + executionData.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); + executionData.dataLoaderNameToBatchCall.get(dataLoaderName) + .add(new GraphQLJavaAgent.ExecutionData.BatchLoadingCall(keys.size(), Thread.currentThread().getName())); + } + + } + } + + public static class DataLoaderHelperDispatchAdvice { + + @Advice.OnMethodExit + public static void dispatch(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoaderHelper, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) DispatchResult dispatchResult) { + try { + // System.out.println("dataloader helper Dispatch " + dataLoaderHelper + " load for execution " + dispatchResult); + // DataLoader dataLoader = getDataLoaderForHelper(dataLoaderHelper); + // // System.out.println("dataLoader: " + dataLoader); + // ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); + // GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + // String dataLoaderName = executionData.dataLoaderToName.get(dataLoader); + // + // executionData.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); + // executionData.dataLoaderNameToBatchCall.get(dataLoaderName).add(new GraphQLJavaAgent.ExecutionData.BatchLoadingCall(dispatchResult.getKeysCount())); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + } + + public static DataLoader getDataLoaderForHelper(Object dataLoaderHelper) { + try { + Field field = dataLoaderHelper.getClass().getDeclaredField("dataLoader"); + field.setAccessible(true); + return (DataLoader) field.get(dataLoaderHelper); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + } class DataFetchingEnvironmentAdvice { @@ -331,31 +392,6 @@ public static void getDataLoader(@Advice.Argument(0) String dataLoaderName, } -class DataLoaderHelperDispatchAdvice { - - @Advice.OnMethodExit - public static void dispatch(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoaderHelper, - @Advice.Return(typing = Assigner.Typing.DYNAMIC) DispatchResult dispatchResult) { - try { - // System.out.println("dataloader helper Dispatch " + dataLoaderHelper + " load for execution " + dispatchResult); - Field field = dataLoaderHelper.getClass().getDeclaredField("dataLoader"); - field.setAccessible(true); - DataLoader dataLoader = (DataLoader) field.get(dataLoaderHelper); - // System.out.println("dataLoader: " + dataLoader); - ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); - String dataLoaderName = executionData.dataLoaderToName.get(dataLoader); - - executionData.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); - executionData.dataLoaderNameToBatchCall.get(dataLoaderName).add(new GraphQLJavaAgent.ExecutionData.BatchLoadingCall(dispatchResult.getKeysCount())); - - } catch (Exception e) { - e.printStackTrace(); - } - - } - -} class DataLoaderLoadAdvice { @@ -374,9 +410,9 @@ class DataLoaderRegistryAdvice { public static void dispatchAll(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoaderRegistry) { List> dataLoaders = ((DataLoaderRegistry) dataLoaderRegistry).getDataLoaders(); ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoaders.get(0)); - System.out.println("calling dispatchAll for " + executionId); } } + From 4db21225779f9906dcbba058612688f1f64bdddb Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 9 Feb 2024 11:49:20 +1100 Subject: [PATCH 158/393] Fix groovy syntax --- .../incremental/DeferExecutionSupportIntegrationTest.groovy | 4 ++-- .../dataloader/DataLoaderPerformanceData.groovy | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 89d9ef7ede..828713794d 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -893,7 +893,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { incrementalResults[2].hasNext == false // every payload has only 1 incremental item, and the data is the same for all of them - incrementalResults.every { it.incremental.size == 1 } + incrementalResults.every { it.incremental.size() == 1 } incrementalResults.every { it.incremental[0].data == [summary: "A summary"] } // "label" is different for every payload @@ -1328,7 +1328,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { incrementalResults[2].hasNext == false // every payload has only 1 incremental item, and the data is the same for all of them - incrementalResults.every { it.incremental.size == 1 } + incrementalResults.every { it.incremental.size() == 1 } incrementalResults.every { it.incremental[0].data == [wordCount: 45999] } // path is different for every payload diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy index 4de47ebcbd..01cf58bba1 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy @@ -122,7 +122,7 @@ class DataLoaderPerformanceData { assert incrementalResults[24].hasNext == false // every payload has only 1 incremental item, and the data is the same for all of them - assert incrementalResults.every { it.incremental.size == 1 } + assert incrementalResults.every { it.incremental.size() == 1 } def incrementalResultsItems = incrementalResults.collect { it.incremental[0] } From 89e741d3c863710928e64c18f0a48d81351a54d1 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 9 Feb 2024 11:50:23 +1100 Subject: [PATCH 159/393] Move gate mechanism from ExecutionInput to GraphQLContext --- src/main/java/graphql/ExecutionInput.java | 29 +------------------ src/main/java/graphql/GraphQLContext.java | 6 ++++ .../execution/AsyncExecutionStrategy.java | 9 ++++-- .../java/graphql/execution/Execution.java | 9 ++++-- .../graphql/execution/ExecutionContext.java | 4 --- .../graphql/execution/ExecutionStrategy.java | 6 +++- .../AsyncExecutionStrategyTest.groovy | 20 +++++-------- ...eferExecutionSupportIntegrationTest.groovy | 3 +- .../DataLoaderPerformanceTest.groovy | 13 +++++---- ...manceWithChainedInstrumentationTest.groovy | 13 +++++---- 10 files changed, 48 insertions(+), 64 deletions(-) diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index 6a82ad61f6..3849777e17 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -29,7 +29,6 @@ public class ExecutionInput { private final DataLoaderRegistry dataLoaderRegistry; private final ExecutionId executionId; private final Locale locale; - private final boolean incrementalSupport; @Internal @@ -45,7 +44,6 @@ private ExecutionInput(Builder builder) { this.locale = builder.locale != null ? builder.locale : Locale.getDefault(); // always have a locale in place this.localContext = builder.localContext; this.extensions = builder.extensions; - this.incrementalSupport = builder.incrementalSupport; } /** @@ -141,16 +139,6 @@ public Map getExtensions() { return extensions; } - /** - * @return whether the execution has support for incremental delivery of data, via the @defer and @stream directives. - * - * This is currently an experimental feature, and only @defer is supported. - */ - @ExperimentalApi - public boolean isIncrementalSupport() { - return incrementalSupport; - } - /** * 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. @@ -171,8 +159,7 @@ public ExecutionInput transform(Consumer builderConsumer) { .variables(this.rawVariables.toMap()) .extensions(this.extensions) .executionId(this.executionId) - .locale(this.locale) - .incrementalSupport(this.incrementalSupport); + .locale(this.locale); builderConsumer.accept(builder); @@ -229,7 +216,6 @@ public static class Builder { private DataLoaderRegistry dataLoaderRegistry = DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY; private Locale locale = Locale.getDefault(); private ExecutionId executionId; - public boolean incrementalSupport = false; public Builder query(String query) { this.query = assertNotNull(query, () -> "query can't be null"); @@ -393,19 +379,6 @@ public Builder dataLoaderRegistry(DataLoaderRegistry dataLoaderRegistry) { return this; } - /** - * @param incrementalSupport whether the execution has support for incremental delivery of data, via the @defer and @stream directives. - *

- * This is currently an experimental feature, and only @defer is supported. - * - * @return this builder - */ - @ExperimentalApi - public Builder incrementalSupport(boolean incrementalSupport) { - this.incrementalSupport = incrementalSupport; - return this; - } - public ExecutionInput build() { return new ExecutionInput(this); } diff --git a/src/main/java/graphql/GraphQLContext.java b/src/main/java/graphql/GraphQLContext.java index 8b913919d3..57651a8d54 100644 --- a/src/main/java/graphql/GraphQLContext.java +++ b/src/main/java/graphql/GraphQLContext.java @@ -42,6 +42,12 @@ public class GraphQLContext { private final ConcurrentMap map; + /** + * The key that should be associated with a boolean value which indicates whether @defer and @stream behaviour is enabled for this execution. + */ + @ExperimentalApi + public static final String ENABLE_INCREMENTAL_SUPPORT = "ENABLE_INCREMENTAL_SUPPORT"; + private GraphQLContext(ConcurrentMap map) { this.map = map; } diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 03fe907bac..0554733dc5 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -4,11 +4,12 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import graphql.ExecutionResult; +import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.incremental.DeferredCall; import graphql.execution.incremental.DeferredCallContext; -import graphql.execution.incremental.IncrementalCall; import graphql.execution.incremental.DeferredExecution; +import graphql.execution.incremental.IncrementalCall; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; @@ -22,6 +23,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; @@ -64,7 +66,10 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); - DeferredExecutionSupport deferredExecutionSupport = executionContext.isIncrementalSupport() ? + DeferredExecutionSupport deferredExecutionSupport = + Optional.ofNullable(executionContext.getGraphQLContext()) + .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) + .orElse(false) ? new DeferredExecutionSupport.DeferredExecutionSupportImpl( fields, parameters, diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 37955a7aaa..621e0e23f2 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; @@ -140,7 +141,9 @@ private CompletableFuture executeOperation(ExecutionContext exe MergedSelectionSet fields = fieldCollector.collectFields( collectorParameters, operationDefinition.getSelectionSet(), - executionContext.isIncrementalSupport() + Optional.ofNullable(executionContext.getGraphQLContext()) + .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) + .orElse(false) ); ResultPath path = ResultPath.rootPath(); @@ -179,13 +182,13 @@ private CompletableFuture executeOperation(ExecutionContext exe result = result.whenComplete(executeOperationCtx::onCompleted); - return deferSupport(executionContext, result); + return incrementalSupport(executionContext, result); } /* * Adds the deferred publisher if it's needed at the end of the query. This is also a good time for the deferred code to start running */ - private CompletableFuture deferSupport(ExecutionContext executionContext, CompletableFuture result) { + private CompletableFuture incrementalSupport(ExecutionContext executionContext, CompletableFuture result) { return result.thenApply(er -> { IncrementalCallState incrementalCallState = executionContext.getIncrementalCallState(); if (incrementalCallState.getIncrementalCallsDetected()) { diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 4c62e58c0b..45deda32a6 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -261,10 +261,6 @@ public IncrementalCallState getIncrementalCallState() { return incrementalCallState; } - public boolean isIncrementalSupport() { - return executionInput != null && executionInput.isIncrementalSupport(); - } - public ExecutionStrategy getStrategy(OperationDefinition.Operation operation) { if (operation == OperationDefinition.Operation.MUTATION) { return getMutationStrategy(); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 05297411cf..0b373e5a1d 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; +import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; import graphql.PublicSpi; @@ -45,6 +46,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.OptionalInt; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -684,7 +686,9 @@ protected CompletableFuture completeValueForObject(ExecutionCon MergedSelectionSet subFields = fieldCollector.collectFields( collectorParameters, parameters.getField(), - executionContext.isIncrementalSupport() + Optional.ofNullable(executionContext.getGraphQLContext()) + .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) + .orElse(false) ); ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull(resolvedObjectType); diff --git a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy index 722b674f1e..b2c84eb076 100644 --- a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy @@ -1,7 +1,6 @@ package graphql.execution import graphql.ErrorType -import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQLContext import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext @@ -34,7 +33,7 @@ import static org.awaitility.Awaitility.await abstract class AsyncExecutionStrategyTest extends Specification { static boolean incrementalSupport - def executionInputMock = Mock(ExecutionInput) + def graphqlContextMock = Mock(GraphQLContext) GraphQLSchema schema(DataFetcher dataFetcher1, DataFetcher dataFetcher2) { def queryName = "RootQueryType" @@ -70,7 +69,7 @@ abstract class AsyncExecutionStrategyTest extends Specification { } def setup() { - executionInputMock.isIncrementalSupport() >> incrementalSupport + graphqlContextMock.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT) >> incrementalSupport } def "execution is serial if the dataFetchers are blocking"() { @@ -106,9 +105,8 @@ abstract class AsyncExecutionStrategyTest extends Specification { .operationDefinition(operation) .instrumentation(SimplePerformantInstrumentation.INSTANCE) .valueUnboxer(ValueUnboxer.DEFAULT) - .graphQLContext(GraphQLContext.getDefault()) + .graphQLContext(graphqlContextMock) .locale(Locale.getDefault()) - .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -149,8 +147,7 @@ abstract class AsyncExecutionStrategyTest extends Specification { .valueUnboxer(ValueUnboxer.DEFAULT) .instrumentation(SimplePerformantInstrumentation.INSTANCE) .locale(Locale.getDefault()) - .graphQLContext(GraphQLContext.getDefault()) - .executionInput(executionInputMock) + .graphQLContext(graphqlContextMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -192,9 +189,8 @@ abstract class AsyncExecutionStrategyTest extends Specification { .operationDefinition(operation) .valueUnboxer(ValueUnboxer.DEFAULT) .instrumentation(SimplePerformantInstrumentation.INSTANCE) - .graphQLContext(GraphQLContext.getDefault()) + .graphQLContext(graphqlContextMock) .locale(Locale.getDefault()) - .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -236,8 +232,7 @@ abstract class AsyncExecutionStrategyTest extends Specification { .instrumentation(SimplePerformantInstrumentation.INSTANCE) .valueUnboxer(ValueUnboxer.DEFAULT) .locale(Locale.getDefault()) - .graphQLContext(GraphQLContext.getDefault()) - .executionInput(executionInputMock) + .graphQLContext(graphqlContextMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -276,7 +271,7 @@ abstract class AsyncExecutionStrategyTest extends Specification { .executionId(ExecutionId.generate()) .operationDefinition(operation) .valueUnboxer(ValueUnboxer.DEFAULT) - .graphQLContext(GraphQLContext.getDefault()) + .graphQLContext(graphqlContextMock) .locale(Locale.getDefault()) .instrumentation(new SimplePerformantInstrumentation() { @@ -299,7 +294,6 @@ abstract class AsyncExecutionStrategyTest extends Specification { } } }) - .executionInput(executionInputMock) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 828713794d..526466041b 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -4,6 +4,7 @@ import graphql.Directives import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQL +import graphql.GraphQLContext import graphql.TestUtil import graphql.execution.pubsub.CapturingSubscriber import graphql.incremental.DelayedIncrementalExecutionResult @@ -1348,7 +1349,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { private ExecutionResult executeQuery(String query, boolean incrementalSupport, Map variables) { return graphQL.execute( ExecutionInput.newExecutionInput() - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .query(query) .variables(variables) .build() diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index 105b455beb..c3c065903b 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -2,6 +2,7 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput import graphql.GraphQL +import graphql.GraphQLContext import graphql.execution.instrumentation.Instrumentation import graphql.incremental.IncrementalExecutionResult import org.dataloader.DataLoaderRegistry @@ -37,7 +38,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -59,7 +60,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -82,7 +83,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -107,7 +108,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -129,7 +130,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(deferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(true) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) .build() IncrementalExecutionResult result = graphQL.execute(executionInput) @@ -155,7 +156,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveDeferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(true) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) .build() IncrementalExecutionResult result = graphQL.execute(executionInput) diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 130f45440f..54cfab1d53 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -2,6 +2,7 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput import graphql.GraphQL +import graphql.GraphQLContext import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation import graphql.incremental.IncrementalExecutionResult @@ -43,7 +44,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -67,7 +68,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -91,7 +92,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -115,7 +116,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(incrementalSupport) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -136,7 +137,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(deferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .incrementalSupport(true) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) .build() IncrementalExecutionResult result = graphQL.execute(executionInput) @@ -159,7 +160,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification def "chainedInstrumentation: data loader will work with deferred queries on multiple levels deep"() { when: ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .incrementalSupport(true) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) .query(expensiveDeferredQuery) .dataLoaderRegistry(dataLoaderRegistry) .build() From 5900e9ced4ac6ca5b350ccb1247bc3173064c404 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 9 Feb 2024 14:15:16 +1100 Subject: [PATCH 160/393] Rename DelayedIncrementalExecutionResult -> DelayedIncrementalPartialResult --- .../java/graphql/execution/Execution.java | 4 ++-- .../incremental/IncrementalCallState.java | 10 ++++---- ...a => DelayedIncrementalPartialResult.java} | 6 ++--- ... DelayedIncrementalPartialResultImpl.java} | 10 ++++---- .../IncrementalExecutionResult.java | 2 +- .../IncrementalExecutionResultImpl.java | 10 ++++---- ...eferExecutionSupportIntegrationTest.groovy | 6 ++--- .../IncrementalCallStateDeferTest.groovy | 24 +++++++++---------- .../DataLoaderPerformanceData.groovy | 6 ++--- .../groovy/readme/IncrementalExamples.java | 6 ++--- 10 files changed, 41 insertions(+), 43 deletions(-) rename src/main/java/graphql/incremental/{DelayedIncrementalExecutionResult.java => DelayedIncrementalPartialResult.java} (88%) rename src/main/java/graphql/incremental/{DelayedIncrementalExecutionResultImpl.java => DelayedIncrementalPartialResultImpl.java} (85%) diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 621e0e23f2..4f6332ce92 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -14,7 +14,7 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.extensions.ExtensionsBuilder; -import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.DelayedIncrementalPartialResult; import graphql.incremental.IncrementalExecutionResultImpl; import graphql.language.Document; import graphql.language.FragmentDefinition; @@ -194,7 +194,7 @@ private CompletableFuture incrementalSupport(ExecutionContext e if (incrementalCallState.getIncrementalCallsDetected()) { // we start the rest of the query now to maximize throughput. We have the initial important results, // and now we can start the rest of the calls as early as possible (even before someone subscribes) - Publisher publisher = incrementalCallState.startDeferredCalls(); + Publisher publisher = incrementalCallState.startDeferredCalls(); return IncrementalExecutionResultImpl.fromExecutionResult(er) // "hasNext" can, in theory, be "false" when all the incremental items are delivered in the diff --git a/src/main/java/graphql/execution/incremental/IncrementalCallState.java b/src/main/java/graphql/execution/incremental/IncrementalCallState.java index e3c9622626..081e0bee87 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalCallState.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCallState.java @@ -2,7 +2,7 @@ import graphql.Internal; import graphql.execution.reactive.SingleSubscriberPublisher; -import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.DelayedIncrementalPartialResult; import graphql.incremental.IncrementalPayload; import graphql.util.LockKit; import org.reactivestreams.Publisher; @@ -14,7 +14,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import static graphql.incremental.DelayedIncrementalExecutionResultImpl.newIncrementalExecutionResult; +import static graphql.incremental.DelayedIncrementalPartialResultImpl.newIncrementalExecutionResult; /** * This provides support for @defer directives on fields that mean that results will be sent AFTER @@ -24,7 +24,7 @@ public class IncrementalCallState { private final AtomicBoolean incrementalCallsDetected = new AtomicBoolean(false); private final Deque> incrementalCalls = new ConcurrentLinkedDeque<>(); - private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); + private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); private final AtomicInteger pendingCalls = new AtomicInteger(); private final LockKit.ReentrantLock publisherLock = new LockKit.ReentrantLock(); @@ -48,7 +48,7 @@ private void drainIncrementalCalls() { try { remainingCalls = pendingCalls.decrementAndGet(); - DelayedIncrementalExecutionResult executionResult = newIncrementalExecutionResult() + DelayedIncrementalPartialResult executionResult = newIncrementalExecutionResult() .incrementalItems(Collections.singletonList(payload)) .hasNext(remainingCalls != 0) .build(); @@ -92,7 +92,7 @@ public boolean getIncrementalCallsDetected() { * * @return the publisher of deferred results */ - public Publisher startDeferredCalls() { + public Publisher startDeferredCalls() { drainIncrementalCalls(); return publisher; } diff --git a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java b/src/main/java/graphql/incremental/DelayedIncrementalPartialResult.java similarity index 88% rename from src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java rename to src/main/java/graphql/incremental/DelayedIncrementalPartialResult.java index 3e618a16ce..706944e528 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalPartialResult.java @@ -10,10 +10,10 @@ * Represents a result that is delivered asynchronously, after the initial {@link IncrementalExecutionResult}. *

* Multiple defer and/or stream payloads (represented by {@link IncrementalPayload}) can be part of the same - * {@link DelayedIncrementalExecutionResult} + * {@link DelayedIncrementalPartialResult} */ @ExperimentalApi -public interface DelayedIncrementalExecutionResult { +public interface DelayedIncrementalPartialResult { /** * @return a list of defer and/or stream payloads. */ @@ -21,7 +21,7 @@ public interface DelayedIncrementalExecutionResult { List getIncremental(); /** - * Indicates whether the stream will continue emitting {@link DelayedIncrementalExecutionResult}s after this one. + * Indicates whether the stream will continue emitting {@link DelayedIncrementalPartialResult}s after this one. *

* The value returned by this method should be "true" for all but the last response in the stream. The value of this * entry is `false` for the last response of the stream. diff --git a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/DelayedIncrementalPartialResultImpl.java similarity index 85% rename from src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java rename to src/main/java/graphql/incremental/DelayedIncrementalPartialResultImpl.java index 3ad40f766f..4a34a56305 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalPartialResultImpl.java @@ -9,12 +9,12 @@ import java.util.stream.Collectors; @ExperimentalApi -public class DelayedIncrementalExecutionResultImpl implements DelayedIncrementalExecutionResult { +public class DelayedIncrementalPartialResultImpl implements DelayedIncrementalPartialResult { private final List incrementalItems; private final boolean hasNext; private final Map extensions; - private DelayedIncrementalExecutionResultImpl(Builder builder) { + private DelayedIncrementalPartialResultImpl(Builder builder) { this.incrementalItems = builder.incrementalItems; this.hasNext = builder.hasNext; this.extensions = builder.extensions; @@ -54,7 +54,7 @@ public Map toSpecification() { } /** - * @return a {@link Builder} that can be used to create an instance of {@link DelayedIncrementalExecutionResultImpl} + * @return a {@link Builder} that can be used to create an instance of {@link DelayedIncrementalPartialResultImpl} */ public static Builder newIncrementalExecutionResult() { return new Builder(); @@ -80,8 +80,8 @@ public Builder extensions(boolean hasNext) { return this; } - public DelayedIncrementalExecutionResultImpl build() { - return new DelayedIncrementalExecutionResultImpl(this); + public DelayedIncrementalPartialResultImpl build() { + return new DelayedIncrementalPartialResultImpl(this); } } } diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResult.java b/src/main/java/graphql/incremental/IncrementalExecutionResult.java index 6794f01185..b3dc1b929e 100644 --- a/src/main/java/graphql/incremental/IncrementalExecutionResult.java +++ b/src/main/java/graphql/incremental/IncrementalExecutionResult.java @@ -107,5 +107,5 @@ public interface IncrementalExecutionResult extends ExecutionResult { * * @return a {@link Publisher} that clients can subscribe to receive incremental payloads. */ - Publisher getIncrementalItemPublisher(); + Publisher getIncrementalItemPublisher(); } diff --git a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java index 8bf8ace96f..765453260d 100644 --- a/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java +++ b/src/main/java/graphql/incremental/IncrementalExecutionResultImpl.java @@ -12,13 +12,11 @@ import java.util.Map; import java.util.stream.Collectors; -import static java.util.stream.Collectors.toList; - @ExperimentalApi public class IncrementalExecutionResultImpl extends ExecutionResultImpl implements IncrementalExecutionResult { private final boolean hasNext; private final List incremental; - private final Publisher incrementalItemPublisher; + private final Publisher incrementalItemPublisher; private IncrementalExecutionResultImpl(Builder builder) { super(builder); @@ -39,7 +37,7 @@ public List getIncremental() { } @Override - public Publisher getIncrementalItemPublisher() { + public Publisher getIncrementalItemPublisher() { return incrementalItemPublisher; } @@ -73,7 +71,7 @@ public Map toSpecification() { public static class Builder extends ExecutionResultImpl.Builder { private boolean hasNext = true; public List incremental; - private Publisher incrementalItemPublisher; + private Publisher incrementalItemPublisher; public Builder hasNext(boolean hasNext) { this.hasNext = hasNext; @@ -85,7 +83,7 @@ public Builder incremental(List incremental) { return this; } - public Builder incrementalItemPublisher(Publisher incrementalItemPublisher) { + public Builder incrementalItemPublisher(Publisher incrementalItemPublisher) { this.incrementalItemPublisher = incrementalItemPublisher; return this; } diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 526466041b..2937c7d2db 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -7,7 +7,7 @@ import graphql.GraphQL import graphql.GraphQLContext import graphql.TestUtil import graphql.execution.pubsub.CapturingSubscriber -import graphql.incremental.DelayedIncrementalExecutionResult +import graphql.incremental.DelayedIncrementalPartialResult import graphql.incremental.IncrementalExecutionResult import graphql.incremental.IncrementalExecutionResultImpl import graphql.schema.DataFetcher @@ -1357,9 +1357,9 @@ class DeferExecutionSupportIntegrationTest extends Specification { } private static List> getIncrementalResults(IncrementalExecutionResult initialResult) { - Publisher deferredResultStream = initialResult.incrementalItemPublisher + Publisher deferredResultStream = initialResult.incrementalItemPublisher - def subscriber = new CapturingSubscriber() + def subscriber = new CapturingSubscriber() deferredResultStream.subscribe(subscriber) diff --git a/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy b/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy index a2b6e9b57d..35bde2440d 100644 --- a/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy @@ -3,7 +3,7 @@ package graphql.execution.incremental import graphql.ExecutionResultImpl import graphql.execution.ResultPath -import graphql.incremental.DelayedIncrementalExecutionResult +import graphql.incremental.DelayedIncrementalPartialResult import org.awaitility.Awaitility import spock.lang.Specification @@ -20,7 +20,7 @@ class IncrementalCallStateDeferTest extends Specification { incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = startAndWaitCalls(incrementalCallState) + List results = startAndWaitCalls(incrementalCallState) then: assertResultsSizeAndHasNextRule(3, results) @@ -37,7 +37,7 @@ class IncrementalCallStateDeferTest extends Specification { incrementalCallState.enqueue(offThreadCallWithinCall(incrementalCallState, "C", "C_Child", 100, "/c")) when: - List results = startAndWaitCalls(incrementalCallState) + List results = startAndWaitCalls(incrementalCallState) then: assertResultsSizeAndHasNextRule(6, results) @@ -57,7 +57,7 @@ class IncrementalCallStateDeferTest extends Specification { incrementalCallState.enqueue(offThread("C", 10, "/field/path")) when: - def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { + def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { @Override void onComplete() { assert false, "This should not be called!" @@ -83,9 +83,9 @@ class IncrementalCallStateDeferTest extends Specification { incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { + def subscriber = new graphql.execution.pubsub.CapturingSubscriber() { @Override - void onNext(DelayedIncrementalExecutionResult executionResult) { + void onNext(DelayedIncrementalPartialResult executionResult) { this.getEvents().add(executionResult) subscription.cancel() this.isDone().set(true) @@ -112,8 +112,8 @@ class IncrementalCallStateDeferTest extends Specification { incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - def subscriber1 = new graphql.execution.pubsub.CapturingSubscriber() - def subscriber2 = new graphql.execution.pubsub.CapturingSubscriber() + def subscriber1 = new graphql.execution.pubsub.CapturingSubscriber() + def subscriber2 = new graphql.execution.pubsub.CapturingSubscriber() incrementalCallState.startDeferredCalls().subscribe(subscriber1) incrementalCallState.startDeferredCalls().subscribe(subscriber2) @@ -184,7 +184,7 @@ class IncrementalCallStateDeferTest extends Specification { incrementalCallState.enqueue(offThread("C", 10, "/field/path")) // <-- will finish first when: - List results = startAndWaitCalls(incrementalCallState) + List results = startAndWaitCalls(incrementalCallState) then: "hasNext placement should be deterministic - only the last event published should have 'hasNext=true'" assertResultsSizeAndHasNextRule(3, results) @@ -226,7 +226,7 @@ class IncrementalCallStateDeferTest extends Specification { return new DeferredCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredCallContext()) } - private static void assertResultsSizeAndHasNextRule(int expectedSize, List results) { + private static void assertResultsSizeAndHasNextRule(int expectedSize, List results) { assert results.size() == expectedSize for (def i = 0; i < results.size(); i++) { @@ -238,8 +238,8 @@ class IncrementalCallStateDeferTest extends Specification { } } - private static List startAndWaitCalls(IncrementalCallState incrementalCallState) { - def subscriber = new graphql.execution.pubsub.CapturingSubscriber() + private static List startAndWaitCalls(IncrementalCallState incrementalCallState) { + def subscriber = new graphql.execution.pubsub.CapturingSubscriber() incrementalCallState.startDeferredCalls().subscribe(subscriber) diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy index 01cf58bba1..88beec433a 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy @@ -4,7 +4,7 @@ import graphql.Directives import graphql.GraphQL import graphql.execution.instrumentation.Instrumentation import graphql.execution.pubsub.CapturingSubscriber -import graphql.incremental.DelayedIncrementalExecutionResult +import graphql.incremental.DelayedIncrementalPartialResult import graphql.incremental.IncrementalExecutionResult import graphql.schema.GraphQLSchema import org.awaitility.Awaitility @@ -339,9 +339,9 @@ class DataLoaderPerformanceData { ] static List> getIncrementalResults(IncrementalExecutionResult initialResult) { - Publisher deferredResultStream = initialResult.incrementalItemPublisher + Publisher deferredResultStream = initialResult.incrementalItemPublisher - def subscriber = new CapturingSubscriber() + def subscriber = new CapturingSubscriber() deferredResultStream.subscribe(subscriber) Awaitility.await().untilTrue(subscriber.isDone()) diff --git a/src/test/groovy/readme/IncrementalExamples.java b/src/test/groovy/readme/IncrementalExamples.java index 0a5f820acd..3b7d1ed7be 100644 --- a/src/test/groovy/readme/IncrementalExamples.java +++ b/src/test/groovy/readme/IncrementalExamples.java @@ -4,7 +4,7 @@ import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; -import graphql.incremental.DelayedIncrementalExecutionResult; +import graphql.incremental.DelayedIncrementalPartialResult; import graphql.incremental.IncrementalExecutionResult; import graphql.schema.GraphQLSchema; import jakarta.servlet.http.HttpServletResponse; @@ -46,7 +46,7 @@ void basicExample(HttpServletResponse httpServletResponse, String deferredQuery) // sendMultipartHttpResult(httpServletResponse, initialResult); - Publisher delayedIncrementalResults = incrementalResult + Publisher delayedIncrementalResults = incrementalResult .getIncrementalItemPublisher(); // @@ -65,7 +65,7 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(DelayedIncrementalExecutionResult executionResult) { + public void onNext(DelayedIncrementalPartialResult executionResult) { // // as each deferred result arrives, send it to where it needs to go // From 6fb8e747b7abb9c3d9b1ce2eaaca593571c18223 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 9 Feb 2024 15:08:47 +1100 Subject: [PATCH 161/393] (temporarily) Remove support for using Data loader to fetch deferred fields --- .../execution/AsyncExecutionStrategy.java | 12 ++------ .../ChainedInstrumentation.java | 6 ++-- .../instrumentation/Instrumentation.java | 9 +++--- .../DataLoaderDispatcherInstrumentation.java | 8 ++++-- .../FieldLevelTrackingApproach.java | 28 ------------------- ...nstrumentationDeferredFieldParameters.java | 28 ------------------- .../DataLoaderPerformanceTest.groovy | 18 ++++++++++++ ...manceWithChainedInstrumentationTest.groovy | 17 +++++++++++ 8 files changed, 49 insertions(+), 77 deletions(-) delete mode 100644 src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 0554733dc5..46839d51cf 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -12,8 +12,6 @@ import graphql.execution.incremental.IncrementalCall; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.incremental.IncrementalPayload; import graphql.util.FpKit; @@ -250,9 +248,8 @@ private Supplier> creat Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationContext fieldCtx = instrumentation.beginDeferredField( - new InstrumentationDeferredFieldParameters(executionContext, callParameters), executionContext.getInstrumentationState() - ); + + instrumentation.beginDeferredField(executionContext.getInstrumentationState()); return dfCache.computeIfAbsent( currentField.getName(), @@ -267,14 +264,9 @@ private Supplier> creat CompletableFuture executionResultCF = fieldValueResult .thenCompose(FieldValueInfo::getFieldValue); - fieldCtx.onDispatched(executionResultCF); - return executionResultCF .thenApply(executionResult -> new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult) - ) - .whenComplete((fieldWithExecutionResult, throwable) -> - fieldCtx.onCompleted(fieldWithExecutionResult.getExecutionResult(), throwable) ); } ) diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 61aa2df85f..49f4ebeaab 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -11,7 +11,6 @@ import graphql.execution.ExecutionContext; import graphql.execution.FieldValueInfo; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -181,14 +180,13 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument @ExperimentalApi @Override - public InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState instrumentationState) { + public InstrumentationContext beginDeferredField(InstrumentationState instrumentationState) { return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() .map(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, instrumentationState); - return instrumentation.beginDeferredField(parameters, specificState); + return instrumentation.beginDeferredField(specificState); }) .collect(Collectors.toList())); - } @Override diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 8143369b22..ba4d6a6c33 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -6,7 +6,6 @@ import graphql.PublicSpi; import graphql.execution.ExecutionContext; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -225,13 +224,15 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen } /** - * This is called just before a deferred field is resolved into a value - * @param parameters the parameters to this step + * This is called just before a deferred field is resolved into a value. + *

+ * This is an EXPERIMENTAL instrumentation callback. The method signature will definitely change. + * * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} * @return a non-null {@link InstrumentationContext} object that will be called back when the step ends */ @ExperimentalApi - default InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { + default InstrumentationContext beginDeferredField(InstrumentationState state) { return noOp(); } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 47ced2ef90..ee30b79250 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -12,7 +12,6 @@ import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -131,7 +130,7 @@ private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContex } @Override - public InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState rawState) { + public InstrumentationContext beginDeferredField(InstrumentationState rawState) { DataLoaderDispatcherInstrumentationState state = ofState(rawState); // // if there are no data loaders, there is nothing to do @@ -139,7 +138,10 @@ public InstrumentationContext beginDeferredField(Instrumentatio if (state.hasNoDataLoaders()) { return ExecutionStrategyInstrumentationContext.NOOP; } - return state.getApproach().beginDeferredField(parameters, state.getState()); + + // The support for @defer in graphql-java is not yet complete. At this time, the resolution of deferred fields + // cannot be done via Data Loaders. + throw new UnsupportedOperationException("Data Loaders cannot be used to resolve deferred fields"); } @Override diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 1773683084..4da094523a 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -8,7 +8,6 @@ import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.parameters.InstrumentationDeferredFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.util.LockKit; @@ -182,33 +181,6 @@ private int getCountForList(List fieldValueInfos) { return result; } - InstrumentationContext beginDeferredField(InstrumentationDeferredFieldParameters parameters, InstrumentationState state) { - // Create a new CallStack, since every deferred fields can execute in parallel. - CallStack callStack = new CallStack(); - int level = parameters.getExecutionStrategyParameters().getPath().getLevel(); - - callStack.lock.runLocked(() -> - callStack.clearAndMarkCurrentLevelAsReady(level) - ); - - return new InstrumentationContext<>() { - @Override - public void onDispatched(CompletableFuture result) { - boolean dispatchNeeded = callStack.lock.callLocked(() -> { - callStack.increaseFetchCount(level); - return dispatchIfNeeded(callStack, level); - }); - if (dispatchNeeded) { - dispatch(); - } - } - - @Override - public void onCompleted(ExecutionResult result, Throwable t) { - } - }; - } - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { CallStack callStack = (CallStack) rawState; ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java deleted file mode 100644 index dd51d088d4..0000000000 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java +++ /dev/null @@ -1,28 +0,0 @@ -package graphql.execution.instrumentation.parameters; - -import graphql.ExperimentalApi; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStrategyParameters; - -/** - * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods - */ -@ExperimentalApi -public class InstrumentationDeferredFieldParameters { - private final ExecutionContext executionContext; - private final ExecutionStrategyParameters executionStrategyParameters; - - public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters) { - this.executionContext = executionContext; - this.executionStrategyParameters = executionStrategyParameters; - } - - - public ExecutionStrategyParameters getExecutionStrategyParameters() { - return executionStrategyParameters; - } - - public ExecutionContext getExecutionContext() { - return executionContext; - } -} diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index c3c065903b..bc8d7bd03e 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -6,6 +6,7 @@ import graphql.GraphQLContext import graphql.execution.instrumentation.Instrumentation import graphql.incremental.IncrementalExecutionResult import org.dataloader.DataLoaderRegistry +import spock.lang.Ignore import spock.lang.Specification import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.assertIncrementalExpensiveData @@ -123,6 +124,22 @@ class DataLoaderPerformanceTest extends Specification { incrementalSupport << [true, false] } + def "data loader will not work with deferred queries"() { + when: + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(deferredQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .build() + + graphQL.execute(executionInput) + + then: + def exception = thrown(UnsupportedOperationException) + exception.message == "Data Loaders cannot be used to resolve deferred fields" + } + + @Ignore("Resolution of deferred fields via Data loaders is not yet supported") def "data loader will work with deferred queries"() { when: @@ -149,6 +166,7 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers.productsForDepartmentsBatchLoaderCounter.get() == 3 } + @Ignore("Resolution of deferred fields via Data loaders is not yet supported") def "data loader will work with deferred queries on multiple levels deep"() { when: diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 54cfab1d53..d72f8b1c38 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -130,6 +130,22 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification incrementalSupport << [true, false] } + def "chainedInstrumentation: data loader will not work with deferred queries"() { + when: + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(deferredQuery) + .dataLoaderRegistry(dataLoaderRegistry) + .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .build() + + graphQL.execute(executionInput) + + then: + def exception = thrown(UnsupportedOperationException) + exception.message == "Data Loaders cannot be used to resolve deferred fields" + } + + @Ignore("Resolution of deferred fields via Data loaders is not yet supported") def "chainedInstrumentation: data loader will work with deferred queries"() { when: @@ -157,6 +173,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification } + @Ignore("Resolution of deferred fields via Data loaders is not yet supported") def "chainedInstrumentation: data loader will work with deferred queries on multiple levels deep"() { when: ExecutionInput executionInput = ExecutionInput.newExecutionInput() From d87a6df1222655b6da540c3c468ed87b4c70b103 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 9 Feb 2024 15:16:20 +1100 Subject: [PATCH 162/393] Rename DeferredCall -> DeferredFragmentCall --- .../execution/AsyncExecutionStrategy.java | 16 ++++----- .../incremental/DeferredCallContext.java | 4 +-- ...redCall.java => DeferredFragmentCall.java} | 6 ++-- .../incremental/DeferredCallTest.groovy | 24 ++++++------- .../IncrementalCallStateDeferTest.groovy | 34 +++++++++---------- 5 files changed, 41 insertions(+), 43 deletions(-) rename src/main/java/graphql/execution/incremental/{DeferredCall.java => DeferredFragmentCall.java} (96%) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 46839d51cf..ab4b609078 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -6,7 +6,7 @@ import graphql.ExecutionResult; import graphql.GraphQLContext; import graphql.PublicApi; -import graphql.execution.incremental.DeferredCall; +import graphql.execution.incremental.DeferredFragmentCall; import graphql.execution.incremental.DeferredCallContext; import graphql.execution.incremental.DeferredExecution; import graphql.execution.incremental.IncrementalCall; @@ -159,7 +159,7 @@ class DeferredExecutionSupportImpl implements DeferredExecutionSupport { private final ExecutionStrategyParameters parameters; private final ExecutionContext executionContext; private final BiFunction> resolveFieldWithInfoFn; - private final Map>> dfCache = new HashMap<>(); + private final Map>> dfCache = new HashMap<>(); private DeferredExecutionSupportImpl( MergedSelectionSet mergedSelectionSet, @@ -207,20 +207,20 @@ public List getNonDeferredFieldNames(List allFieldNames) { @Override public Set> createCalls() { return deferredExecutionToFields.keySet().stream() - .map(this::createDeferredCall) + .map(this::createDeferredFragmentCall) .collect(Collectors.toSet()); } - private DeferredCall createDeferredCall(DeferredExecution deferredExecution) { + private DeferredFragmentCall createDeferredFragmentCall(DeferredExecution deferredExecution) { DeferredCallContext deferredCallContext = new DeferredCallContext(); List mergedFields = deferredExecutionToFields.get(deferredExecution); - List>> calls = mergedFields.stream() + List>> calls = mergedFields.stream() .map(currentField -> this.createResultSupplier(currentField, deferredCallContext)) .collect(Collectors.toList()); - return new DeferredCall( + return new DeferredFragmentCall( deferredExecution.getLabel(), this.parameters.getPath(), calls, @@ -228,7 +228,7 @@ private DeferredCall createDeferredCall(DeferredExecution deferredExecution) { ); } - private Supplier> createResultSupplier( + private Supplier> createResultSupplier( MergedField currentField, DeferredCallContext deferredCallContext ) { @@ -266,7 +266,7 @@ private Supplier> creat return executionResultCF .thenApply(executionResult -> - new DeferredCall.FieldWithExecutionResult(currentField.getName(), executionResult) + new DeferredFragmentCall.FieldWithExecutionResult(currentField.getName(), executionResult) ); } ) diff --git a/src/main/java/graphql/execution/incremental/DeferredCallContext.java b/src/main/java/graphql/execution/incremental/DeferredCallContext.java index 9596799ba2..15f428966f 100644 --- a/src/main/java/graphql/execution/incremental/DeferredCallContext.java +++ b/src/main/java/graphql/execution/incremental/DeferredCallContext.java @@ -10,12 +10,12 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * Contains data relevant to the execution of a {@link DeferredCall}. + * Contains data relevant to the execution of a {@link DeferredFragmentCall}. *

* The responsibilities of this class are similar to {@link graphql.execution.ExecutionContext}, but restricted to the * execution of a deferred call (instead of the whole GraphQL execution like {@link graphql.execution.ExecutionContext}). *

- * Some behaviours, like error capturing, need to be scoped to a single {@link DeferredCall}, because each defer payload + * Some behaviours, like error capturing, need to be scoped to a single {@link DeferredFragmentCall}, because each defer payload * contains its own distinct list of errors. */ @Internal diff --git a/src/main/java/graphql/execution/incremental/DeferredCall.java b/src/main/java/graphql/execution/incremental/DeferredFragmentCall.java similarity index 96% rename from src/main/java/graphql/execution/incremental/DeferredCall.java rename to src/main/java/graphql/execution/incremental/DeferredFragmentCall.java index fce9d974f3..be28fb9e90 100644 --- a/src/main/java/graphql/execution/incremental/DeferredCall.java +++ b/src/main/java/graphql/execution/incremental/DeferredFragmentCall.java @@ -37,7 +37,7 @@ * Will result on 1 instance of `DeferredCall`, containing calls for the 2 fields: "text" and "summary". */ @Internal -public class DeferredCall implements IncrementalCall { +public class DeferredFragmentCall implements IncrementalCall { private final String label; public ResultPath getPath() { @@ -48,7 +48,7 @@ public ResultPath getPath() { private final List>> calls; private final DeferredCallContext deferredCallContext; - public DeferredCall( + public DeferredFragmentCall( String label, ResultPath path, List>> calls, @@ -77,7 +77,7 @@ public CompletableFuture invoke() { /** * Non-nullable errors need special treatment. * When they happen, all the sibling fields will be ignored in the result. So as soon as one of the field calls - * throw this error, we can ignore the {@link ExecutionResult} from all the fields associated with this {@link DeferredCall} + * throw this error, we can ignore the {@link ExecutionResult} from all the fields associated with this {@link DeferredFragmentCall} * and build a special {@link DeferPayload} that captures the details of the error. */ private DeferPayload handleNonNullableFieldError(DeferPayload result, Throwable throwable) { diff --git a/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy index 310e70f2a2..252d6ca449 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy @@ -4,8 +4,6 @@ import graphql.ExecutionResultImpl import graphql.GraphQLError import graphql.execution.NonNullableFieldWasNullException import graphql.execution.ResultPath -import graphql.execution.incremental.DeferredCall -import graphql.execution.incremental.DeferredCallContext import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -18,7 +16,7 @@ class DeferredCallTest extends Specification { def "test call capture gives a CF"() { given: - DeferredCall call = new DeferredCall("my-label", parse("/path"), + DeferredFragmentCall call = new DeferredFragmentCall("my-label", parse("/path"), [createResolvedFieldCall("field", "some data")], new DeferredCallContext()) when: @@ -33,7 +31,7 @@ class DeferredCallTest extends Specification { def "multiple field calls are resolved together"() { given: - DeferredCall call = new DeferredCall("my-label", parse("/path"), + DeferredFragmentCall call = new DeferredFragmentCall("my-label", parse("/path"), [ createResolvedFieldCall("field1", "some data 1"), createResolvedFieldCall("field2", "some data 2"), @@ -60,7 +58,7 @@ class DeferredCallTest extends Specification { getPath() >> ResultPath.parse("/path") } - DeferredCall call = new DeferredCall("my-label", parse("/path"), [ + DeferredFragmentCall call = new DeferredFragmentCall("my-label", parse("/path"), [ createFieldCallThatThrowsException(mockedException), createResolvedFieldCall("field1", "some data") ], deferredCallContext) @@ -83,23 +81,23 @@ class DeferredCallTest extends Specification { ] } - private static Supplier> createResolvedFieldCall( + private static Supplier> createResolvedFieldCall( String fieldName, Object data ) { return createResolvedFieldCall(fieldName, data, Collections.emptyList()) } - private static Supplier> createResolvedFieldCall( + private static Supplier> createResolvedFieldCall( String fieldName, Object data, List errors ) { - return new Supplier>() { + return new Supplier>() { @Override - CompletableFuture get() { + CompletableFuture get() { return completedFuture( - new DeferredCall.FieldWithExecutionResult(fieldName, + new DeferredFragmentCall.FieldWithExecutionResult(fieldName, new ExecutionResultImpl(data, errors) ) ) @@ -107,12 +105,12 @@ class DeferredCallTest extends Specification { } } - private static Supplier> createFieldCallThatThrowsException( + private static Supplier> createFieldCallThatThrowsException( Throwable exception ) { - return new Supplier>() { + return new Supplier>() { @Override - CompletableFuture get() { + CompletableFuture get() { return CompletableFuture.failedFuture(exception) } } diff --git a/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy b/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy index 35bde2440d..8634458122 100644 --- a/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/IncrementalCallStateDeferTest.groovy @@ -142,27 +142,27 @@ class IncrementalCallStateDeferTest extends Specification { def "multiple fields are part of the same call"() { given: "a DeferredCall that contains resolution of multiple fields" - def call1 = new Supplier>() { + def call1 = new Supplier>() { @Override - CompletableFuture get() { + CompletableFuture get() { return CompletableFuture.supplyAsync({ Thread.sleep(10) - new DeferredCall.FieldWithExecutionResult("call1", new ExecutionResultImpl("Call 1", [])) + new DeferredFragmentCall.FieldWithExecutionResult("call1", new ExecutionResultImpl("Call 1", [])) }) } } - def call2 = new Supplier>() { + def call2 = new Supplier>() { @Override - CompletableFuture get() { + CompletableFuture get() { return CompletableFuture.supplyAsync({ Thread.sleep(100) - new DeferredCall.FieldWithExecutionResult("call2", new ExecutionResultImpl("Call 2", [])) + new DeferredFragmentCall.FieldWithExecutionResult("call2", new ExecutionResultImpl("Call 2", [])) }) } } - def deferredCall = new DeferredCall(null, ResultPath.parse("/field/path"), [call1, call2], new DeferredCallContext()) + def deferredCall = new DeferredFragmentCall(null, ResultPath.parse("/field/path"), [call1, call2], new DeferredCallContext()) when: def incrementalCallState = new IncrementalCallState() @@ -195,35 +195,35 @@ class IncrementalCallStateDeferTest extends Specification { results.any { it.incremental[0].data["c"] == "C" } } - private static DeferredCall offThread(String data, int sleepTime, String path) { - def callSupplier = new Supplier>() { + private static DeferredFragmentCall offThread(String data, int sleepTime, String path) { + def callSupplier = new Supplier>() { @Override - CompletableFuture get() { + CompletableFuture get() { return CompletableFuture.supplyAsync({ Thread.sleep(sleepTime) if (data == "Bang") { throw new RuntimeException(data) } - new DeferredCall.FieldWithExecutionResult(data.toLowerCase(), new ExecutionResultImpl(data, [])) + new DeferredFragmentCall.FieldWithExecutionResult(data.toLowerCase(), new ExecutionResultImpl(data, [])) }) } } - return new DeferredCall(null, ResultPath.parse(path), [callSupplier], new DeferredCallContext()) + return new DeferredFragmentCall(null, ResultPath.parse(path), [callSupplier], new DeferredCallContext()) } - private static DeferredCall offThreadCallWithinCall(IncrementalCallState incrementalCallState, String dataParent, String dataChild, int sleepTime, String path) { - def callSupplier = new Supplier>() { + private static DeferredFragmentCall offThreadCallWithinCall(IncrementalCallState incrementalCallState, String dataParent, String dataChild, int sleepTime, String path) { + def callSupplier = new Supplier>() { @Override - CompletableFuture get() { + CompletableFuture get() { CompletableFuture.supplyAsync({ Thread.sleep(sleepTime) incrementalCallState.enqueue(offThread(dataChild, sleepTime, path)) - new DeferredCall.FieldWithExecutionResult(dataParent.toLowerCase(), new ExecutionResultImpl(dataParent, [])) + new DeferredFragmentCall.FieldWithExecutionResult(dataParent.toLowerCase(), new ExecutionResultImpl(dataParent, [])) }) } } - return new DeferredCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredCallContext()) + return new DeferredFragmentCall(null, ResultPath.parse("/field/path"), [callSupplier], new DeferredCallContext()) } private static void assertResultsSizeAndHasNextRule(int expectedSize, List results) { From bfafd99c94549357bcedd69b81f8c0bcb1f4a0a9 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 9 Feb 2024 15:20:53 +1100 Subject: [PATCH 163/393] Move DeferredExecutionSupport into its own class file --- .../execution/AsyncExecutionStrategy.java | 197 +---------------- .../incremental/DeferredExecutionSupport.java | 201 ++++++++++++++++++ 2 files changed, 203 insertions(+), 195 deletions(-) create mode 100644 src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index ab4b609078..d5eb5c782a 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -1,35 +1,17 @@ package graphql.execution; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableSet; import graphql.ExecutionResult; import graphql.GraphQLContext; import graphql.PublicApi; -import graphql.execution.incremental.DeferredFragmentCall; -import graphql.execution.incremental.DeferredCallContext; -import graphql.execution.incremental.DeferredExecution; -import graphql.execution.incremental.IncrementalCall; +import graphql.execution.incremental.DeferredExecutionSupport; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import graphql.incremental.IncrementalPayload; -import graphql.util.FpKit; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import static graphql.execution.MergedSelectionSet.newMergedSelectionSet; /** * The standard graphql execution strategy that runs fields asynchronously non-blocking. @@ -115,7 +97,7 @@ public CompletableFuture execute(ExecutionContext executionCont } executionStrategyCtx.onFieldValuesInfo(completeValueInfos); executionResultFutures.await().whenComplete(handleResultsConsumer); - }).exceptionally((ex) -> { + }).exceptionally(ex -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. @@ -127,179 +109,4 @@ public CompletableFuture execute(ExecutionContext executionCont overallResult.whenComplete(executionStrategyCtx::onCompleted); return overallResult; } - - /** - * The purpose of this class hierarchy is to encapsulate most of the logic for deferring field execution, thus - * keeping the main execution strategy code clean and focused on the main execution logic. - *

- * The {@link NoOp} instance should be used when incremental support is not enabled for the current execution. The - * methods in this class will return empty or no-op results, that should not impact the main execution. - *

- * {@link DeferredExecutionSupportImpl} is the actual implementation that will be used when incremental support is enabled. - */ - private interface DeferredExecutionSupport { - - boolean isDeferredField(MergedField mergedField); - - int deferredFieldsCount(); - - List getNonDeferredFieldNames(List allFieldNames); - - Set> createCalls(); - - DeferredExecutionSupport NOOP = new DeferredExecutionSupport.NoOp(); - - /** - * An implementation that actually executes the deferred fields. - */ - class DeferredExecutionSupportImpl implements DeferredExecutionSupport { - private final ImmutableListMultimap deferredExecutionToFields; - private final ImmutableSet deferredFields; - private final ImmutableList nonDeferredFieldNames; - private final ExecutionStrategyParameters parameters; - private final ExecutionContext executionContext; - private final BiFunction> resolveFieldWithInfoFn; - private final Map>> dfCache = new HashMap<>(); - - private DeferredExecutionSupportImpl( - MergedSelectionSet mergedSelectionSet, - ExecutionStrategyParameters parameters, - ExecutionContext executionContext, - BiFunction> resolveFieldWithInfoFn - ) { - this.executionContext = executionContext; - this.resolveFieldWithInfoFn = resolveFieldWithInfoFn; - ImmutableListMultimap.Builder deferredExecutionToFieldsBuilder = ImmutableListMultimap.builder(); - ImmutableSet.Builder deferredFieldsBuilder = ImmutableSet.builder(); - ImmutableList.Builder nonDeferredFieldNamesBuilder = ImmutableList.builder(); - - mergedSelectionSet.getSubFields().values().forEach(mergedField -> { - mergedField.getDeferredExecutions().forEach(de -> { - deferredExecutionToFieldsBuilder.put(de, mergedField); - deferredFieldsBuilder.add(mergedField); - }); - - if (mergedField.getDeferredExecutions().isEmpty()) { - nonDeferredFieldNamesBuilder.add(mergedField.getSingleField().getResultKey()); - } - }); - - this.deferredExecutionToFields = deferredExecutionToFieldsBuilder.build(); - this.deferredFields = deferredFieldsBuilder.build(); - this.parameters = parameters; - this.nonDeferredFieldNames = nonDeferredFieldNamesBuilder.build(); - } - - @Override - public boolean isDeferredField(MergedField mergedField) { - return deferredFields.contains(mergedField); - } - - @Override - public int deferredFieldsCount() { - return deferredFields.size(); - } - - @Override - public List getNonDeferredFieldNames(List allFieldNames) { - return this.nonDeferredFieldNames; - } - @Override - public Set> createCalls() { - return deferredExecutionToFields.keySet().stream() - .map(this::createDeferredFragmentCall) - .collect(Collectors.toSet()); - } - - private DeferredFragmentCall createDeferredFragmentCall(DeferredExecution deferredExecution) { - DeferredCallContext deferredCallContext = new DeferredCallContext(); - - List mergedFields = deferredExecutionToFields.get(deferredExecution); - - List>> calls = mergedFields.stream() - .map(currentField -> this.createResultSupplier(currentField, deferredCallContext)) - .collect(Collectors.toList()); - - return new DeferredFragmentCall( - deferredExecution.getLabel(), - this.parameters.getPath(), - calls, - deferredCallContext - ); - } - - private Supplier> createResultSupplier( - MergedField currentField, - DeferredCallContext deferredCallContext - ) { - Map fields = new LinkedHashMap<>(); - fields.put(currentField.getName(), currentField); - - ExecutionStrategyParameters callParameters = parameters.transform(builder -> - { - MergedSelectionSet mergedSelectionSet = newMergedSelectionSet().subFields(fields).build(); - builder.deferredCallContext(deferredCallContext) - .field(currentField) - .fields(mergedSelectionSet) - .path(parameters.getPath().segment(currentField.getName())) - .parent(null); // this is a break in the parent -> child chain - it's a new start effectively - } - ); - - - Instrumentation instrumentation = executionContext.getInstrumentation(); - - instrumentation.beginDeferredField(executionContext.getInstrumentationState()); - - return dfCache.computeIfAbsent( - currentField.getName(), - // The same field can be associated with multiple defer executions, so - // we memoize the field resolution to avoid multiple calls to the same data fetcher - key -> FpKit.interThreadMemoize(() -> { - CompletableFuture fieldValueResult = resolveFieldWithInfoFn - .apply(executionContext, callParameters); - - // Create a reference to the CompletableFuture that resolves an ExecutionResult - // so we can pass it to the Instrumentation "onDispatched" callback. - CompletableFuture executionResultCF = fieldValueResult - .thenCompose(FieldValueInfo::getFieldValue); - - return executionResultCF - .thenApply(executionResult -> - new DeferredFragmentCall.FieldWithExecutionResult(currentField.getName(), executionResult) - ); - } - ) - ); - } - } - - /** - * A no-op implementation that should be used when incremental support is not enabled for the current execution. - */ - class NoOp implements DeferredExecutionSupport { - - @Override - public boolean isDeferredField(MergedField mergedField) { - return false; - } - - @Override - public int deferredFieldsCount() { - return 0; - } - - @Override - public List getNonDeferredFieldNames(List allFieldNames) { - return allFieldNames; - } - - @Override - public Set> createCalls() { - return Collections.emptySet(); - } - } - } - - } diff --git a/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java new file mode 100644 index 0000000000..1689afa684 --- /dev/null +++ b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java @@ -0,0 +1,201 @@ +package graphql.execution.incremental; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import graphql.ExecutionResult; +import graphql.Internal; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.FieldValueInfo; +import graphql.execution.MergedField; +import graphql.execution.MergedSelectionSet; +import graphql.execution.instrumentation.Instrumentation; +import graphql.incremental.IncrementalPayload; +import graphql.util.FpKit; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * The purpose of this class hierarchy is to encapsulate most of the logic for deferring field execution, thus + * keeping the main execution strategy code clean and focused on the main execution logic. + *

+ * The {@link NoOp} instance should be used when incremental support is not enabled for the current execution. The + * methods in this class will return empty or no-op results, that should not impact the main execution. + *

+ * {@link DeferredExecutionSupportImpl} is the actual implementation that will be used when incremental support is enabled. + */ +@Internal +public interface DeferredExecutionSupport { + + boolean isDeferredField(MergedField mergedField); + + int deferredFieldsCount(); + + List getNonDeferredFieldNames(List allFieldNames); + + Set> createCalls(); + + DeferredExecutionSupport NOOP = new DeferredExecutionSupport.NoOp(); + + /** + * An implementation that actually executes the deferred fields. + */ + class DeferredExecutionSupportImpl implements DeferredExecutionSupport { + private final ImmutableListMultimap deferredExecutionToFields; + private final ImmutableSet deferredFields; + private final ImmutableList nonDeferredFieldNames; + private final ExecutionStrategyParameters parameters; + private final ExecutionContext executionContext; + private final BiFunction> resolveFieldWithInfoFn; + private final Map>> dfCache = new HashMap<>(); + + public DeferredExecutionSupportImpl( + MergedSelectionSet mergedSelectionSet, + ExecutionStrategyParameters parameters, + ExecutionContext executionContext, + BiFunction> resolveFieldWithInfoFn + ) { + this.executionContext = executionContext; + this.resolveFieldWithInfoFn = resolveFieldWithInfoFn; + ImmutableListMultimap.Builder deferredExecutionToFieldsBuilder = ImmutableListMultimap.builder(); + ImmutableSet.Builder deferredFieldsBuilder = ImmutableSet.builder(); + ImmutableList.Builder nonDeferredFieldNamesBuilder = ImmutableList.builder(); + + mergedSelectionSet.getSubFields().values().forEach(mergedField -> { + mergedField.getDeferredExecutions().forEach(de -> { + deferredExecutionToFieldsBuilder.put(de, mergedField); + deferredFieldsBuilder.add(mergedField); + }); + + if (mergedField.getDeferredExecutions().isEmpty()) { + nonDeferredFieldNamesBuilder.add(mergedField.getSingleField().getResultKey()); + } + }); + + this.deferredExecutionToFields = deferredExecutionToFieldsBuilder.build(); + this.deferredFields = deferredFieldsBuilder.build(); + this.parameters = parameters; + this.nonDeferredFieldNames = nonDeferredFieldNamesBuilder.build(); + } + + @Override + public boolean isDeferredField(MergedField mergedField) { + return deferredFields.contains(mergedField); + } + + @Override + public int deferredFieldsCount() { + return deferredFields.size(); + } + + @Override + public List getNonDeferredFieldNames(List allFieldNames) { + return this.nonDeferredFieldNames; + } + + @Override + public Set> createCalls() { + return deferredExecutionToFields.keySet().stream() + .map(this::createDeferredFragmentCall) + .collect(Collectors.toSet()); + } + + private DeferredFragmentCall createDeferredFragmentCall(DeferredExecution deferredExecution) { + DeferredCallContext deferredCallContext = new DeferredCallContext(); + + List mergedFields = deferredExecutionToFields.get(deferredExecution); + + List>> calls = mergedFields.stream() + .map(currentField -> this.createResultSupplier(currentField, deferredCallContext)) + .collect(Collectors.toList()); + + return new DeferredFragmentCall( + deferredExecution.getLabel(), + this.parameters.getPath(), + calls, + deferredCallContext + ); + } + + private Supplier> createResultSupplier( + MergedField currentField, + DeferredCallContext deferredCallContext + ) { + Map fields = new LinkedHashMap<>(); + fields.put(currentField.getName(), currentField); + + ExecutionStrategyParameters callParameters = parameters.transform(builder -> + { + MergedSelectionSet mergedSelectionSet = MergedSelectionSet.newMergedSelectionSet().subFields(fields).build(); + builder.deferredCallContext(deferredCallContext) + .field(currentField) + .fields(mergedSelectionSet) + .path(parameters.getPath().segment(currentField.getName())) + .parent(null); // this is a break in the parent -> child chain - it's a new start effectively + } + ); + + + Instrumentation instrumentation = executionContext.getInstrumentation(); + + instrumentation.beginDeferredField(executionContext.getInstrumentationState()); + + return dfCache.computeIfAbsent( + currentField.getName(), + // The same field can be associated with multiple defer executions, so + // we memoize the field resolution to avoid multiple calls to the same data fetcher + key -> FpKit.interThreadMemoize(() -> { + CompletableFuture fieldValueResult = resolveFieldWithInfoFn + .apply(executionContext, callParameters); + + // Create a reference to the CompletableFuture that resolves an ExecutionResult + // so we can pass it to the Instrumentation "onDispatched" callback. + CompletableFuture executionResultCF = fieldValueResult + .thenCompose(FieldValueInfo::getFieldValue); + + return executionResultCF + .thenApply(executionResult -> + new DeferredFragmentCall.FieldWithExecutionResult(currentField.getName(), executionResult) + ); + } + ) + ); + } + } + + /** + * A no-op implementation that should be used when incremental support is not enabled for the current execution. + */ + class NoOp implements DeferredExecutionSupport { + + @Override + public boolean isDeferredField(MergedField mergedField) { + return false; + } + + @Override + public int deferredFieldsCount() { + return 0; + } + + @Override + public List getNonDeferredFieldNames(List allFieldNames) { + return allFieldNames; + } + + @Override + public Set> createCalls() { + return Collections.emptySet(); + } + } +} From 00a18627ab08f9455dabe2382d3880fb70802082 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 9 Feb 2024 16:24:57 +1100 Subject: [PATCH 164/393] Adjustments after merging --- .../execution/AsyncExecutionStrategy.java | 17 +------ .../graphql/execution/ExecutionStrategy.java | 47 ++++++++++++++++--- .../ChainedInstrumentation.java | 12 ++--- ...ecutionStrategyInstrumentationContext.java | 7 --- .../instrumentation/Instrumentation.java | 4 +- 5 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 379ad3ee14..96419730cf 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -1,7 +1,6 @@ package graphql.execution; import graphql.ExecutionResult; -import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.incremental.DeferredExecutionSupport; import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; @@ -9,7 +8,6 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import java.util.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; @@ -46,20 +44,9 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); - DeferredExecutionSupport deferredExecutionSupport = - Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) - .orElse(false) ? - new DeferredExecutionSupport.DeferredExecutionSupportImpl( - fields, - parameters, - executionContext, - this::resolveFieldWithInfo - ) : DeferredExecutionSupport.NOOP; + DeferredExecutionSupport deferredExecutionSupport = createDeferredExecutionSupport(executionContext, parameters); + Async.CombinedBuilder futures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport); - executionContext.getIncrementalCallState().enqueue(deferredExecutionSupport.createCalls()); - - Async.CombinedBuilder futures = getAsyncFieldValueInfo(executionContext, parameters); CompletableFuture overallResult = new CompletableFuture<>(); executionStrategyCtx.onDispatched(overallResult); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5c9e514d59..2d0fb0c66b 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -15,9 +15,10 @@ import graphql.collect.ImmutableKit; import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; +import graphql.execution.incremental.DeferredExecutionSupport; +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; @@ -200,12 +201,17 @@ protected CompletableFuture> executeObject(ExecutionContext ); List fieldNames = parameters.getFields().getKeys(); - Async.CombinedBuilder resolvedFieldFutures = getAsyncFieldValueInfo(executionContext, parameters); + + DeferredExecutionSupport deferredExecutionSupport = createDeferredExecutionSupport(executionContext, parameters); + Async.CombinedBuilder resolvedFieldFutures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport); + CompletableFuture> overallResult = new CompletableFuture<>(); resolveObjectCtx.onDispatched(overallResult); resolvedFieldFutures.await().whenComplete((completeValueInfos, throwable) -> { - BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldNames, overallResult,executionContext); + List fieldsExecutedOnInitialResult = deferredExecutionSupport.getNonDeferredFieldNames(fieldNames); + + BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldsExecutedOnInitialResult, overallResult,executionContext); if (throwable != null) { handleResultsConsumer.accept(null, throwable); return; @@ -246,10 +252,35 @@ private BiConsumer, Throwable> buildFieldValueMap(List fiel }; } + DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + MergedSelectionSet fields = parameters.getFields(); + + return Optional.ofNullable(executionContext.getGraphQLContext()) + .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) + .orElse(false) ? + new DeferredExecutionSupport.DeferredExecutionSupportImpl( + fields, + parameters, + executionContext, + this::resolveFieldWithInfo + ) : DeferredExecutionSupport.NOOP; + + } + @NotNull - Async.CombinedBuilder getAsyncFieldValueInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + Async.CombinedBuilder getAsyncFieldValueInfo( + ExecutionContext executionContext, + ExecutionStrategyParameters parameters, + DeferredExecutionSupport deferredExecutionSupport + ) { MergedSelectionSet fields = parameters.getFields(); - Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size()); + + executionContext.getIncrementalCallState().enqueue(deferredExecutionSupport.createCalls()); + + // Only non-deferred fields should be considered for calculating the expected size of futures. + Async.CombinedBuilder futures = Async + .ofExpectedSize(fields.size() - deferredExecutionSupport.deferredFieldsCount()); + for (String fieldName : fields.getKeys()) { MergedField currentField = fields.getSubField(fieldName); @@ -257,8 +288,10 @@ Async.CombinedBuilder getAsyncFieldValueInfo(ExecutionContext ex ExecutionStrategyParameters newParameters = parameters .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); - CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); - futures.add(future); + if (!deferredExecutionSupport.isDeferredField(currentField)) { + CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); + futures.add(future); + } } return futures; } diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index c92d62c41a..35847dfdde 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -195,7 +195,7 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument @ExperimentalApi @Override - public InstrumentationContext beginDeferredField(InstrumentationState instrumentationState) { + public InstrumentationContext beginDeferredField(InstrumentationState instrumentationState) { return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() .map(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, instrumentationState); @@ -511,21 +511,21 @@ public void onFieldValuesException() { } } - private static class ChainedDeferredExecutionStrategyInstrumentationContext implements InstrumentationContext { + private static class ChainedDeferredExecutionStrategyInstrumentationContext implements InstrumentationContext { - private final List> contexts; + private final List> contexts; - ChainedDeferredExecutionStrategyInstrumentationContext(List> contexts) { + ChainedDeferredExecutionStrategyInstrumentationContext(List> contexts) { this.contexts = Collections.unmodifiableList(contexts); } @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched(CompletableFuture result) { contexts.forEach(context -> context.onDispatched(result)); } @Override - public void onCompleted(ExecutionResult result, Throwable t) { + public void onCompleted(Object result, Throwable t) { contexts.forEach(context -> context.onCompleted(result, t)); } } diff --git a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java index 6b418fb073..04fbceab81 100644 --- a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java @@ -1,11 +1,9 @@ package graphql.execution.instrumentation; import graphql.ExecutionResult; -import graphql.ExperimentalApi; import graphql.Internal; import graphql.PublicSpi; import graphql.execution.FieldValueInfo; -import graphql.execution.MergedField; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -22,11 +20,6 @@ default void onFieldValuesException() { } - @ExperimentalApi - default void onDeferredField(MergedField field) { - - } - /** * This creates a no-op {@link InstrumentationContext} if the one pass in is null * diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 13700f1948..bce13bbd34 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -248,8 +248,8 @@ default ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationEx * @return a nullable {@link ExecutionStrategyInstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @ExperimentalApi - default ExecuteObjectInstrumentationContext beginDeferredField(InstrumentationState state) { - return ExecuteObjectInstrumentationContext.NOOP; + default InstrumentationContext beginDeferredField(InstrumentationState state) { + return noOp(); } /** From 768eddddbb132c2a7fbc32f166763a740d485622 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 11 Feb 2024 15:44:23 +1100 Subject: [PATCH 165/393] Tidy up wording ahead of minor spec improvement https://github.com/graphql/graphql-spec/pull/1073 --- src/main/java/graphql/GraphQLError.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/GraphQLError.java b/src/main/java/graphql/GraphQLError.java index b4fc6fa600..7cd1f3a934 100644 --- a/src/main/java/graphql/GraphQLError.java +++ b/src/main/java/graphql/GraphQLError.java @@ -39,8 +39,8 @@ public interface GraphQLError extends Serializable { ErrorClassification getErrorType(); /** - * The graphql spec says that the (optional) path field of any error should be a list - * of path entries https://spec.graphql.org/October2021/#sec-Handling-Field-Errors + * The graphql spec says that the (optional) path field of any error must be a list + * of path entries https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format * * @return the path in list format */ From 8e5250b678273e5dbd6954c779d6d7dc061423f0 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 11 Feb 2024 16:15:59 +1100 Subject: [PATCH 166/393] Add JSON escape for single line description and test --- .../java/graphql/language/AstPrinter.java | 2 +- .../groovy/graphql/parser/ParserTest.groovy | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index 218dabd59e..5070a4f139 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -534,7 +534,7 @@ private String description(Node node) { if (description.isMultiLine()) { s = "\"\"\"" + (startNewLine ? "" : "\n") + description.getContent() + "\n\"\"\"\n"; } else { - s = "\"" + description.getContent() + "\"\n"; + s = "\"" + escapeJsonString(description.getContent()) + "\"\n"; } return s; } diff --git a/src/test/groovy/graphql/parser/ParserTest.groovy b/src/test/groovy/graphql/parser/ParserTest.groovy index 4484eb9e58..00b8fef455 100644 --- a/src/test/groovy/graphql/parser/ParserTest.groovy +++ b/src/test/groovy/graphql/parser/ParserTest.groovy @@ -4,6 +4,7 @@ package graphql.parser import graphql.language.Argument import graphql.language.ArrayValue import graphql.language.AstComparator +import graphql.language.AstPrinter import graphql.language.BooleanValue import graphql.language.Description import graphql.language.Directive @@ -1151,5 +1152,35 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" document.getDefinitions()[0].getSourceLocation() == SourceLocation.EMPTY } + def "escape characters correctly printed when printing AST"() { + given: + def src = "\"\\\"\" scalar A" + + def env = newParserEnvironment() + .document(src) + .parserOptions( + ParserOptions.newParserOptions() + .captureIgnoredChars(true) + .build() + ) + .build() + + when: + // Parse the original Document + def doc = Parser.parse(env) + // Print the AST + def printed = AstPrinter.printAst(doc) + // Re-parse printed AST + def reparsed = Parser.parse(printed) + + then: + noExceptionThrown() // The printed AST was re-parsed without exception + + when: + def reparsedPrinted = AstPrinter.printAst(reparsed) + + then: + reparsedPrinted == printed // Re-parsing and re-printing produces the same result + } } From 3c2de830ae84fd0ddefbdd3c960827c89e550f49 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 11 Feb 2024 16:29:58 +1100 Subject: [PATCH 167/393] Tidy error comment --- src/main/java/graphql/GraphQLError.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/GraphQLError.java b/src/main/java/graphql/GraphQLError.java index 7cd1f3a934..c18752b14a 100644 --- a/src/main/java/graphql/GraphQLError.java +++ b/src/main/java/graphql/GraphQLError.java @@ -39,8 +39,10 @@ public interface GraphQLError extends Serializable { ErrorClassification getErrorType(); /** - * The graphql spec says that the (optional) path field of any error must be a list - * of path entries https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format + * The graphql spec says that the (optional) path field of any error must be + * a list of path entries starting at the root of the response + * and ending with the field associated with the error + * https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format * * @return the path in list format */ From a801d205b5b939074847180d30ca38c537e9caf5 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 12 Feb 2024 17:26:58 +1100 Subject: [PATCH 168/393] Move ENABLE_INCREMENTAL_SUPPORT to @ExperimentalApi --- src/main/java/graphql/ExperimentalApi.java | 4 ++++ src/main/java/graphql/GraphQLContext.java | 6 ------ src/main/java/graphql/execution/Execution.java | 3 ++- .../graphql/execution/ExecutionStrategy.java | 6 +++--- .../execution/AsyncExecutionStrategyTest.groovy | 4 +++- .../DeferExecutionSupportIntegrationTest.groovy | 3 ++- .../dataloader/DataLoaderPerformanceTest.groovy | 16 +++++++++------- ...formanceWithChainedInstrumentationTest.groovy | 16 +++++++++------- 8 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/main/java/graphql/ExperimentalApi.java b/src/main/java/graphql/ExperimentalApi.java index 991932b2d4..80be253cd1 100644 --- a/src/main/java/graphql/ExperimentalApi.java +++ b/src/main/java/graphql/ExperimentalApi.java @@ -20,4 +20,8 @@ @Target(value = {CONSTRUCTOR, METHOD, TYPE, FIELD}) @Documented public @interface ExperimentalApi { + /** + * The key that should be associated with a boolean value which indicates whether @defer and @stream behaviour is enabled for this execution. + */ + String ENABLE_INCREMENTAL_SUPPORT = "ENABLE_INCREMENTAL_SUPPORT"; } diff --git a/src/main/java/graphql/GraphQLContext.java b/src/main/java/graphql/GraphQLContext.java index 57651a8d54..8b913919d3 100644 --- a/src/main/java/graphql/GraphQLContext.java +++ b/src/main/java/graphql/GraphQLContext.java @@ -42,12 +42,6 @@ public class GraphQLContext { private final ConcurrentMap map; - /** - * The key that should be associated with a boolean value which indicates whether @defer and @stream behaviour is enabled for this execution. - */ - @ExperimentalApi - public static final String ENABLE_INCREMENTAL_SUPPORT = "ENABLE_INCREMENTAL_SUPPORT"; - private GraphQLContext(ConcurrentMap map) { this.map = map; } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 4f6332ce92..6199e0b3f0 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -4,6 +4,7 @@ import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; +import graphql.ExperimentalApi; import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; @@ -142,7 +143,7 @@ private CompletableFuture executeOperation(ExecutionContext exe collectorParameters, operationDefinition.getSelectionSet(), Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) + .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) .orElse(false) ); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 2d0fb0c66b..0a92931226 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -4,7 +4,7 @@ import com.google.common.collect.Maps; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; -import graphql.GraphQLContext; +import graphql.ExperimentalApi; import graphql.GraphQLError; import graphql.Internal; import graphql.PublicSpi; @@ -256,7 +256,7 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi MergedSelectionSet fields = parameters.getFields(); return Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) + .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) .orElse(false) ? new DeferredExecutionSupport.DeferredExecutionSupportImpl( fields, @@ -839,7 +839,7 @@ protected CompletableFuture> completeValueForObject(Executio collectorParameters, parameters.getField(), Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT)) + .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) .orElse(false) ); diff --git a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy index b2c84eb076..2c9bbf5263 100644 --- a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy @@ -2,6 +2,7 @@ package graphql.execution import graphql.ErrorType import graphql.ExecutionResult +import graphql.ExperimentalApi import graphql.GraphQLContext import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext import graphql.execution.instrumentation.InstrumentationState @@ -22,6 +23,7 @@ import java.util.concurrent.CompletionException import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock +import static graphql.ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT import static graphql.Scalars.GraphQLString import static graphql.TestUtil.mergedField import static graphql.TestUtil.mergedSelectionSet @@ -69,7 +71,7 @@ abstract class AsyncExecutionStrategyTest extends Specification { } def setup() { - graphqlContextMock.get(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT) >> incrementalSupport + graphqlContextMock.get(ENABLE_INCREMENTAL_SUPPORT) >> incrementalSupport } def "execution is serial if the dataFetchers are blocking"() { diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 2937c7d2db..4b31de0edf 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -3,6 +3,7 @@ package graphql.execution.incremental import graphql.Directives import graphql.ExecutionInput import graphql.ExecutionResult +import graphql.ExperimentalApi import graphql.GraphQL import graphql.GraphQLContext import graphql.TestUtil @@ -1349,7 +1350,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { private ExecutionResult executeQuery(String query, boolean incrementalSupport, Map variables) { return graphQL.execute( ExecutionInput.newExecutionInput() - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .query(query) .variables(variables) .build() diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index bc8d7bd03e..f71673a5f4 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -1,6 +1,7 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput +import graphql.ExperimentalApi import graphql.GraphQL import graphql.GraphQLContext import graphql.execution.instrumentation.Instrumentation @@ -9,6 +10,7 @@ import org.dataloader.DataLoaderRegistry import spock.lang.Ignore import spock.lang.Specification +import static graphql.ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.assertIncrementalExpensiveData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedExpensiveData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialDeferredData @@ -39,7 +41,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -61,7 +63,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -84,7 +86,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -109,7 +111,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -129,7 +131,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(deferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): true]) .build() graphQL.execute(executionInput) @@ -147,7 +149,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(deferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): true]) .build() IncrementalExecutionResult result = graphQL.execute(executionInput) @@ -174,7 +176,7 @@ class DataLoaderPerformanceTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveDeferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): true]) .build() IncrementalExecutionResult result = graphQL.execute(executionInput) diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index d72f8b1c38..2b22854956 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -1,6 +1,7 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput +import graphql.ExperimentalApi import graphql.GraphQL import graphql.GraphQLContext import graphql.execution.instrumentation.ChainedInstrumentation @@ -10,6 +11,7 @@ import org.dataloader.DataLoaderRegistry import spock.lang.Ignore import spock.lang.Specification +import static graphql.ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.assertIncrementalExpensiveData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedExpensiveData import static graphql.execution.instrumentation.dataloader.DataLoaderPerformanceData.expectedInitialDeferredData @@ -44,7 +46,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -68,7 +70,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -92,7 +94,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -116,7 +118,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(expensiveQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): incrementalSupport]) .build() def result = graphQL.execute(executionInput) @@ -135,7 +137,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(deferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): true]) .build() graphQL.execute(executionInput) @@ -153,7 +155,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(deferredQuery) .dataLoaderRegistry(dataLoaderRegistry) - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): true]) .build() IncrementalExecutionResult result = graphQL.execute(executionInput) @@ -177,7 +179,7 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification def "chainedInstrumentation: data loader will work with deferred queries on multiple levels deep"() { when: ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .graphQLContext([(GraphQLContext.ENABLE_INCREMENTAL_SUPPORT): true]) + .graphQLContext([(ENABLE_INCREMENTAL_SUPPORT): true]) .query(expensiveDeferredQuery) .dataLoaderRegistry(dataLoaderRegistry) .build() From 99c5d0f6e0175946a1362ad8f4cdc43ccb313cc7 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 12 Feb 2024 17:28:31 +1100 Subject: [PATCH 169/393] Remove unnecessary isEmpty check --- .../graphql/execution/incremental/IncrementalCallState.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/incremental/IncrementalCallState.java b/src/main/java/graphql/execution/incremental/IncrementalCallState.java index 081e0bee87..f96a706f36 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalCallState.java +++ b/src/main/java/graphql/execution/incremental/IncrementalCallState.java @@ -78,9 +78,7 @@ public void enqueue(IncrementalCall incrementalCal } public void enqueue(Collection> calls) { - if (!calls.isEmpty()) { - calls.forEach(this::enqueue); - } + calls.forEach(this::enqueue); } public boolean getIncrementalCallsDetected() { From b126bafc411904a28997640cca61bde70a33147a Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 12 Feb 2024 17:33:58 +1100 Subject: [PATCH 170/393] Add test with label having 'null' value --- ...eferExecutionSupportIntegrationTest.groovy | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 4b31de0edf..c805132853 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -344,6 +344,44 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "defer with null label should behave as if no label was provided"() { + def query = ''' + query { + post { + id + ... @defer(label: null) { + summary + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [post: [id: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path : ["post"], + data : [summary: "A summary"] + ] + ] + ] + ] + } + def "deferred field results in 'null'"() { def query = ''' query { From eaf28799133957a603cb3d173f70927dc876902d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:26:22 +0000 Subject: [PATCH 171/393] Bump org.eclipse.jetty:jetty-server from 11.0.15 to 11.0.20 Bumps org.eclipse.jetty:jetty-server from 11.0.15 to 11.0.20. --- updated-dependencies: - dependency-name: org.eclipse.jetty:jetty-server dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b0b2172c78..77fc672842 100644 --- a/build.gradle +++ b/build.gradle @@ -109,7 +109,7 @@ dependencies { testImplementation 'org.codehaus.groovy:groovy:3.0.20' testImplementation 'org.codehaus.groovy:groovy-json:3.0.20' testImplementation 'com.google.code.gson:gson:2.10.1' - testImplementation 'org.eclipse.jetty:jetty-server:11.0.15' + testImplementation 'org.eclipse.jetty:jetty-server:11.0.20' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' testImplementation 'org.awaitility:awaitility-groovy:4.2.0' testImplementation 'com.github.javafaker:javafaker:1.0.2' From 23d94b62eed60cbcac9d370a9ecc4342a064ffc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:59:03 +0000 Subject: [PATCH 172/393] Bump google-github-actions/auth from 2.1.0 to 2.1.1 Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: google-github-actions/auth dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/invoke_test_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index b955074934..ea710cb715 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -50,7 +50,7 @@ jobs: - id: 'auth' name: 'Authenticate to Google Cloud' - uses: google-github-actions/auth@v2.1.0 + uses: google-github-actions/auth@v2.1.1 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} From 3f85c59d7021c532c1e1f114a5e94af30420100f Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 13 Feb 2024 14:00:39 +1000 Subject: [PATCH 173/393] wip --- .../execution/AsyncExecutionStrategy.java | 4 + .../execution/DataLoaderDispatchStrategy.java | 52 +++++ .../graphql/execution/ExecutionStrategy.java | 205 ++++++++---------- 3 files changed, 147 insertions(+), 114 deletions(-) create mode 100644 src/main/java/graphql/execution/DataLoaderDispatchStrategy.java diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 11d7239c3e..3de35d18ca 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -37,6 +37,8 @@ public AsyncExecutionStrategy(DataFetcherExceptionHandler exceptionHandler) { @SuppressWarnings("FutureReturnValueIgnored") public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + dataLoaderDispatcherStrategy.executionStrategy(executionContext, parameters); + Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); @@ -58,12 +60,14 @@ public CompletableFuture execute(ExecutionContext executionCont for (FieldValueInfo completeValueInfo : completeValueInfos) { fieldValuesFutures.add(completeValueInfo.getFieldValueFuture()); } + dataLoaderDispatcherStrategy.executionStrategy_onFieldValuesInfo(completeValueInfos); executionStrategyCtx.onFieldValuesInfo(completeValueInfos); fieldValuesFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. + dataLoaderDispatcherStrategy.executeObject_onFieldValuesException(ex); executionStrategyCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; diff --git a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java new file mode 100644 index 0000000000..ec239e59e0 --- /dev/null +++ b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java @@ -0,0 +1,52 @@ +package graphql.execution; + +import graphql.Internal; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +@Internal +public interface DataLoaderDispatchStrategy { + + public static final DataLoaderDispatchStrategy NO_OP = new DataLoaderDispatchStrategy() { + }; + + + default void executionStrategy(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + + } + + default void executionStrategy_onFieldValuesInfo(List fieldValueInfoList) { + + } + + default void executionStrategy_onFieldValuesException(Throwable t) { + + } + + + default void executeObject(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters) { + + } + + default void executeObject_onFieldValuesInfo(List fieldValueInfoList) { + + } + + default void executeObject_onFieldValuesException(Throwable t) { + + } + + default void fieldFetched(ExecutionContext executionContext, + Supplier dataFetchingEnvironment, + ExecutionStrategyParameters executionStrategyParameters, + DataFetcher dataFetcher, + CompletableFuture fetchedValue) { + + } + + +} diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index edd01a981e..9f1d45b1ea 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -14,9 +14,9 @@ import graphql.collect.ImmutableKit; import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; @@ -133,6 +133,8 @@ public abstract class ExecutionStrategy { protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; private final ResolveType resolvedType = new ResolveType(); + protected final DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; + /** * The default execution strategy constructor uses the {@link SimpleDataFetcherExceptionHandler} * for data fetching errors. @@ -172,9 +174,7 @@ public static String mkNameForPath(List currentField) { * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object - * * @return a promise to an {@link ExecutionResult} - * * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ public abstract CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException; @@ -184,9 +184,7 @@ public static String mkNameForPath(List currentField) { * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object - * * @return a promise to a map of object field values - * * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ protected CompletableFuture> executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { @@ -194,7 +192,7 @@ protected CompletableFuture> executeObject(ExecutionContext InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); ExecuteObjectInstrumentationContext resolveObjectCtx = ExecuteObjectInstrumentationContext.nonNullCtx( - instrumentation.beginExecuteObject(instrumentationParameters, executionContext.getInstrumentationState()) + instrumentation.beginExecuteObject(instrumentationParameters, executionContext.getInstrumentationState()) ); List fieldNames = parameters.getFields().getKeys(); @@ -203,7 +201,7 @@ protected CompletableFuture> executeObject(ExecutionContext resolveObjectCtx.onDispatched(overallResult); resolvedFieldFutures.await().whenComplete((completeValueInfos, throwable) -> { - BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldNames, overallResult,executionContext); + BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldNames, overallResult, executionContext); if (throwable != null) { handleResultsConsumer.accept(null, throwable); return; @@ -253,7 +251,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo(ExecutionContext ex ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); ExecutionStrategyParameters newParameters = parameters - .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); + .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); futures.add(future); @@ -272,9 +270,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo(ExecutionContext ex * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object - * * @return a promise to an {@link Object} - * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ protected CompletableFuture resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -292,9 +288,7 @@ protected CompletableFuture resolveField(ExecutionContext executionConte * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object - * * @return a promise to a {@link FieldValueInfo} - * * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value */ protected CompletableFuture resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -303,12 +297,12 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationContext fieldCtx = nonNullCtx(instrumentation.beginFieldExecution( - new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() + new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() )); CompletableFuture fetchFieldFuture = fetchField(executionContext, parameters); CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> - completeField(executionContext, parameters, fetchedValue)); + completeField(executionContext, parameters, fetchedValue)); CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); @@ -326,9 +320,7 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object - * * @return a promise to a fetched object - * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ protected CompletableFuture fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -342,7 +334,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC Supplier dataFetchingEnvironment = FpKit.intraThreadMemoize(() -> { Supplier executionStepInfo = FpKit.intraThreadMemoize( - () -> createExecutionStepInfo(executionContext, parameters, fieldDef, parentType)); + () -> createExecutionStepInfo(executionContext, parameters, fieldDef, parentType)); Supplier> argumentValues = () -> executionStepInfo.get().getArguments(); @@ -351,24 +343,24 @@ protected CompletableFuture fetchField(ExecutionContext executionC // DataFetchingFieldSelectionSet and QueryDirectives is a supplier of sorts - eg a lazy pattern DataFetchingFieldSelectionSet fieldCollector = DataFetchingFieldSelectionSetImpl.newCollector(executionContext.getGraphQLSchema(), fieldDef.getType(), normalizedFieldSupplier); QueryDirectives queryDirectives = new QueryDirectivesImpl(field, - executionContext.getGraphQLSchema(), - executionContext.getCoercedVariables().toMap(), - executionContext.getGraphQLContext(), - executionContext.getLocale()); + executionContext.getGraphQLSchema(), + executionContext.getCoercedVariables().toMap(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); return newDataFetchingEnvironment(executionContext) - .source(parameters.getSource()) - .localContext(parameters.getLocalContext()) - .arguments(argumentValues) - .fieldDefinition(fieldDef) - .mergedField(parameters.getField()) - .fieldType(fieldDef.getType()) - .executionStepInfo(executionStepInfo) - .parentType(parentType) - .selectionSet(fieldCollector) - .queryDirectives(queryDirectives) - .build(); + .source(parameters.getSource()) + .localContext(parameters.getLocalContext()) + .arguments(argumentValues) + .fieldDefinition(fieldDef) + .mergedField(parameters.getField()) + .fieldType(fieldDef.getType()) + .executionStepInfo(executionStepInfo) + .parentType(parentType) + .selectionSet(fieldCollector) + .queryDirectives(queryDirectives) + .build(); }); DataFetcher dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); @@ -376,7 +368,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, dataFetcher instanceof TrivialDataFetcher); InstrumentationContext fetchCtx = nonNullCtx(instrumentation.beginFieldFetch(instrumentationFieldFetchParams, - executionContext.getInstrumentationState()) + executionContext.getInstrumentationState()) ); dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); @@ -384,17 +376,17 @@ protected CompletableFuture fetchField(ExecutionContext executionC fetchCtx.onDispatched(fetchedValue); return fetchedValue - .handle((result, exception) -> { - fetchCtx.onCompleted(result, exception); - if (exception != null) { - return handleFetchingException(dataFetchingEnvironment.get(), exception); - } else { - // we can simply return the fetched value CF and avoid a allocation - return fetchedValue; - } - }) - .thenCompose(Function.identity()) - .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); + .handle((result, exception) -> { + fetchCtx.onCompleted(result, exception); + if (exception != null) { + return handleFetchingException(dataFetchingEnvironment.get(), exception); + } else { + // we can simply return the fetched value CF and avoid a allocation + return fetchedValue; + } + }) + .thenCompose(Function.identity()) + .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); } private CompletableFuture invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { @@ -433,17 +425,17 @@ protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext execution localContext = parameters.getLocalContext(); } return FetchedValue.newFetchedValue() - .fetchedValue(executionContext.getValueUnboxer().unbox(dataFetcherResult.getData())) - .rawFetchedValue(dataFetcherResult.getData()) - .errors(dataFetcherResult.getErrors()) - .localContext(localContext) - .build(); + .fetchedValue(executionContext.getValueUnboxer().unbox(dataFetcherResult.getData())) + .rawFetchedValue(dataFetcherResult.getData()) + .errors(dataFetcherResult.getErrors()) + .localContext(localContext) + .build(); } else { return FetchedValue.newFetchedValue() - .fetchedValue(executionContext.getValueUnboxer().unbox(result)) - .rawFetchedValue(result) - .localContext(parameters.getLocalContext()) - .build(); + .fetchedValue(executionContext.getValueUnboxer().unbox(result)) + .rawFetchedValue(result) + .localContext(parameters.getLocalContext()) + .build(); } } @@ -458,20 +450,20 @@ private void addExtensionsIfPresent(ExecutionContext executionContext, DataFetch } protected CompletableFuture handleFetchingException( - DataFetchingEnvironment environment, - Throwable e) { + DataFetchingEnvironment environment, + Throwable e) { DataFetcherExceptionHandlerParameters handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() - .dataFetchingEnvironment(environment) - .exception(e) - .build(); + .dataFetchingEnvironment(environment) + .exception(e) + .build(); try { return asyncHandleException(dataFetcherExceptionHandler, handlerParameters); } catch (Exception handlerException) { handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() - .dataFetchingEnvironment(environment) - .exception(handlerException) - .build(); + .dataFetchingEnvironment(environment) + .exception(handlerException) + .build(); return asyncHandleException(new SimpleDataFetcherExceptionHandler(), handlerParameters); } } @@ -479,7 +471,7 @@ protected CompletableFuture handleFetchingException( private CompletableFuture asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) { //noinspection unchecked return handler.handleException(handlerParameters).thenApply( - handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build() + handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build() ); } @@ -495,9 +487,7 @@ private CompletableFuture asyncHandleException(DataFetcherExceptionHandle * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param fetchedValue the fetched raw value - * * @return a {@link FieldValueInfo} - * * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value */ protected FieldValueInfo completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) { @@ -509,16 +499,16 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, fetchedValue); InstrumentationContext ctxCompleteField = nonNullCtx(instrumentation.beginFieldCompletion( - instrumentationParams, executionContext.getInstrumentationState() + instrumentationParams, executionContext.getInstrumentationState() )); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo); ExecutionStrategyParameters newParameters = parameters.transform(builder -> - builder.executionStepInfo(executionStepInfo) - .source(fetchedValue.getFetchedValue()) - .localContext(fetchedValue.getLocalContext()) - .nonNullFieldValidator(nonNullableFieldValidator) + builder.executionStepInfo(executionStepInfo) + .source(fetchedValue.getFetchedValue()) + .localContext(fetchedValue.getLocalContext()) + .nonNullFieldValidator(nonNullableFieldValidator) ); FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters); @@ -540,9 +530,7 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object - * * @return a {@link FieldValueInfo} - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected FieldValueInfo completeValue(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { @@ -589,9 +577,7 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra * Called to complete a null value. * * @param parameters contains the parameters holding the fields to be executed and source object - * * @return a {@link FieldValueInfo} - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters parameters) { @@ -613,7 +599,6 @@ protected CompletableFuture completeValueForNull(ExecutionStrategyParame * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param result the result to complete, raw result - * * @return a {@link FieldValueInfo} */ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Object result) { @@ -636,7 +621,6 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param iterableValues the values to complete, can't be null - * * @return a {@link FieldValueInfo} */ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Iterable iterableValues) { @@ -648,7 +632,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationContext completeListCtx = nonNullCtx(instrumentation.beginFieldListCompletion( - instrumentationParams, executionContext.getInstrumentationState() + instrumentationParams, executionContext.getInstrumentationState() )); List fieldValueInfos = new ArrayList<>(size.orElse(1)); @@ -663,11 +647,11 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, FetchedValue value = unboxPossibleDataFetcherResult(executionContext, parameters, item); ExecutionStrategyParameters newParameters = parameters.transform(builder -> - builder.executionStepInfo(stepInfoForListElement) - .nonNullFieldValidator(nonNullableFieldValidator) - .localContext(value.getLocalContext()) - .path(indexedPath) - .source(value.getFetchedValue()) + builder.executionStepInfo(stepInfoForListElement) + .nonNullFieldValidator(nonNullableFieldValidator) + .localContext(value.getLocalContext()) + .path(indexedPath) + .source(value.getFetchedValue()) ); fieldValueInfos.add(completeValue(executionContext, newParameters)); index++; @@ -690,9 +674,9 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, }); return FieldValueInfo.newFieldValueInfo(LIST) - .fieldValue(overallResult) - .fieldValueInfos(fieldValueInfos) - .build(); + .fieldValue(overallResult) + .fieldValueInfos(fieldValueInfos) + .build(); } protected void handleValueException(CompletableFuture overallResult, Throwable e, ExecutionContext executionContext) { @@ -725,7 +709,6 @@ protected void handleValueException(CompletableFuture overallResult, Thro * @param parameters contains the parameters holding the fields to be executed and source object * @param scalarType the type of the scalar * @param result the result to be coerced - * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { @@ -751,7 +734,6 @@ protected CompletableFuture completeValueForScalar(ExecutionContext exec * @param parameters contains the parameters holding the fields to be executed and source object * @param enumType the type of the enum * @param result the result to be coerced - * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { @@ -776,19 +758,18 @@ protected CompletableFuture completeValueForEnum(ExecutionContext execut * @param parameters contains the parameters holding the fields to be executed and source object * @param resolvedObjectType the resolved object type * @param result the result to be coerced - * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture> completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); FieldCollectorParameters collectorParameters = newParameters() - .schema(executionContext.getGraphQLSchema()) - .objectType(resolvedObjectType) - .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getCoercedVariables().toMap()) - .graphQLContext(executionContext.getGraphQLContext()) - .build(); + .schema(executionContext.getGraphQLSchema()) + .objectType(resolvedObjectType) + .fragments(executionContext.getFragmentsByName()) + .variables(executionContext.getCoercedVariables().toMap()) + .graphQLContext(executionContext.getGraphQLContext()) + .build(); MergedSelectionSet subFields = fieldCollector.collectFields(collectorParameters, parameters.getField()); @@ -796,10 +777,10 @@ protected CompletableFuture> completeValueForObject(Executio NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, newExecutionStepInfo); ExecutionStrategyParameters newParameters = parameters.transform(builder -> - builder.executionStepInfo(newExecutionStepInfo) - .fields(subFields) - .nonNullFieldValidator(nonNullableFieldValidator) - .source(result) + builder.executionStepInfo(newExecutionStepInfo) + .fields(subFields) + .nonNullFieldValidator(nonNullableFieldValidator) + .source(result) ); // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. @@ -841,7 +822,6 @@ private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrate * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param field the field to find the definition of - * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Field field) { @@ -855,7 +835,6 @@ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, * @param schema the schema in play * @param parentType the parent type of the field * @param field the field to find the definition of - * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObjectType parentType, Field field) { @@ -873,7 +852,6 @@ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObject * * @param e this indicates that a null value was returned for a non null field, which needs to cause the parent field * to become null OR continue on as an exception - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected void assertNonNullFieldPrecondition(NonNullableFieldWasNullException e) throws NonNullableFieldWasNullException { @@ -920,7 +898,6 @@ protected ExecutionResult handleNonNullException(ExecutionContext executionConte * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldDefinition the field definition to build type info for * @param fieldContainer the field container - * * @return a new type info */ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionContext, @@ -939,23 +916,23 @@ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionCo List fieldArgs = field.getArguments(); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); Supplier> argValuesSupplier = () -> ValuesResolver.getArgumentValues(codeRegistry, - fieldArgDefs, - fieldArgs, - executionContext.getCoercedVariables(), - executionContext.getGraphQLContext(), - executionContext.getLocale()); + fieldArgDefs, + fieldArgs, + executionContext.getCoercedVariables(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); argumentValues = FpKit.intraThreadMemoize(argValuesSupplier); } return newExecutionStepInfo() - .type(fieldType) - .fieldDefinition(fieldDefinition) - .fieldContainer(fieldContainer) - .field(field) - .path(parameters.getPath()) - .parentInfo(parentStepInfo) - .arguments(argumentValues) - .build(); + .type(fieldType) + .fieldDefinition(fieldDefinition) + .fieldContainer(fieldContainer) + .field(field) + .path(parameters.getPath()) + .parentInfo(parentStepInfo) + .arguments(argumentValues) + .build(); } } From 9e3192350746dcb2523c1f55716c746d57b2b71b Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 13 Feb 2024 17:01:15 +1000 Subject: [PATCH 174/393] wip --- .../execution/AsyncExecutionStrategy.java | 5 +- .../execution/DataLoaderDispatchStrategy.java | 16 +- .../java/graphql/execution/Execution.java | 1 + .../graphql/execution/ExecutionStrategy.java | 29 ++- .../FallbackDataLoaderDispatchStrategy.java | 31 +++ .../PerLevelDataLoaderDispatchStrategy.java | 220 ++++++++++++++++++ 6 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 src/main/java/graphql/execution/instrumentation/dataloader/FallbackDataLoaderDispatchStrategy.java create mode 100644 src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 3de35d18ca..4397fbfc99 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -36,7 +36,6 @@ public AsyncExecutionStrategy(DataFetcherExceptionHandler exceptionHandler) { @Override @SuppressWarnings("FutureReturnValueIgnored") public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { - dataLoaderDispatcherStrategy.executionStrategy(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); @@ -60,14 +59,14 @@ public CompletableFuture execute(ExecutionContext executionCont for (FieldValueInfo completeValueInfo : completeValueInfos) { fieldValuesFutures.add(completeValueInfo.getFieldValueFuture()); } - dataLoaderDispatcherStrategy.executionStrategy_onFieldValuesInfo(completeValueInfos); + dataLoaderDispatcherStrategy.executionStrategy_onFieldValuesInfo(completeValueInfos, parameters); executionStrategyCtx.onFieldValuesInfo(completeValueInfos); fieldValuesFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. - dataLoaderDispatcherStrategy.executeObject_onFieldValuesException(ex); + dataLoaderDispatcherStrategy.executeObject_onFieldValuesException(ex, parameters); executionStrategyCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; diff --git a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java index ec239e59e0..20b851e09f 100644 --- a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java @@ -2,16 +2,14 @@ import graphql.Internal; import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; @Internal public interface DataLoaderDispatchStrategy { - public static final DataLoaderDispatchStrategy NO_OP = new DataLoaderDispatchStrategy() { + DataLoaderDispatchStrategy NO_OP = new DataLoaderDispatchStrategy() { }; @@ -19,11 +17,11 @@ default void executionStrategy(ExecutionContext executionContext, ExecutionStrat } - default void executionStrategy_onFieldValuesInfo(List fieldValueInfoList) { + default void executionStrategy_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { } - default void executionStrategy_onFieldValuesException(Throwable t) { + default void executionStrategy_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { } @@ -32,16 +30,15 @@ default void executeObject(ExecutionContext executionContext, ExecutionStrategyP } - default void executeObject_onFieldValuesInfo(List fieldValueInfoList) { + default void executeObject_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { } - default void executeObject_onFieldValuesException(Throwable t) { + default void executeObject_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { } default void fieldFetched(ExecutionContext executionContext, - Supplier dataFetchingEnvironment, ExecutionStrategyParameters executionStrategyParameters, DataFetcher dataFetcher, CompletableFuture fetchedValue) { @@ -49,4 +46,7 @@ default void fieldFetched(ExecutionContext executionContext, } + default DataFetcher modifyDataFetcher(DataFetcher dataFetcher) { + return dataFetcher; + } } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 133ef2cda9..ac38a276cf 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -151,6 +151,7 @@ private CompletableFuture executeOperation(ExecutionContext exe CompletableFuture result; try { ExecutionStrategy executionStrategy = executionContext.getStrategy(operation); + executionStrategy.initBeforeExecution(executionContext); result = executionStrategy.execute(executionContext, parameters); } catch (NonNullableFieldWasNullException e) { // this means it was non-null types all the way from an offending non-null type diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 9f1d45b1ea..c1f4480030 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -17,6 +17,9 @@ import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationState; +import graphql.execution.instrumentation.dataloader.FallbackDataLoaderDispatchStrategy; +import graphql.execution.instrumentation.dataloader.PerLevelDataLoaderDispatchStrategy; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; @@ -133,7 +136,7 @@ public abstract class ExecutionStrategy { protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; private final ResolveType resolvedType = new ResolveType(); - protected final DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; + protected DataLoaderDispatchStrategy dataLoaderDispatcherStrategy; /** * The default execution strategy constructor uses the {@link SimpleDataFetcherExceptionHandler} @@ -143,6 +146,7 @@ protected ExecutionStrategy() { dataFetcherExceptionHandler = new SimpleDataFetcherExceptionHandler(); } + /** * The consumers of the execution strategy can pass in a {@link DataFetcherExceptionHandler} to better * decide what do when a data fetching error happens @@ -153,6 +157,23 @@ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHand this.dataFetcherExceptionHandler = dataFetcherExceptionHandler; } + public void initBeforeExecution(ExecutionContext executionContext) { + initDataLoaderStrategy(executionContext); + } + + private void initDataLoaderStrategy(ExecutionContext executionContext) { + if (executionContext.getDataLoaderRegistry() == DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY) { + this.dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; + return; + } + if (this instanceof AsyncExecutionStrategy) { + this.dataLoaderDispatcherStrategy = new PerLevelDataLoaderDispatchStrategy(executionContext); + } else { + this.dataLoaderDispatcherStrategy = new FallbackDataLoaderDispatchStrategy(executionContext); + } + } + + @Internal public static String mkNameForPath(Field currentField) { return mkNameForPath(Collections.singletonList(currentField)); @@ -188,6 +209,7 @@ public static String mkNameForPath(List currentField) { * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ protected CompletableFuture> executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + dataLoaderDispatcherStrategy.executeObject(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); @@ -211,12 +233,14 @@ protected CompletableFuture> executeObject(ExecutionContext for (FieldValueInfo completeValueInfo : completeValueInfos) { resultFutures.add(completeValueInfo.getFieldValueFuture()); } + dataLoaderDispatcherStrategy.executeObject_onFieldValuesInfo(completeValueInfos, parameters); resolveObjectCtx.onFieldValuesInfo(completeValueInfos); resultFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. + dataLoaderDispatcherStrategy.executeObject_onFieldValuesException(ex, parameters); resolveObjectCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; @@ -371,9 +395,10 @@ protected CompletableFuture fetchField(ExecutionContext executionC executionContext.getInstrumentationState()) ); + dataFetcher = dataLoaderDispatcherStrategy.modifyDataFetcher(dataFetcher); dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); - + dataLoaderDispatcherStrategy.fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); fetchCtx.onDispatched(fetchedValue); return fetchedValue .handle((result, exception) -> { diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FallbackDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/FallbackDataLoaderDispatchStrategy.java new file mode 100644 index 0000000000..dba4378046 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FallbackDataLoaderDispatchStrategy.java @@ -0,0 +1,31 @@ +package graphql.execution.instrumentation.dataloader; + +import graphql.Internal; +import graphql.execution.DataLoaderDispatchStrategy; +import graphql.execution.ExecutionContext; +import graphql.schema.DataFetcher; + + +/** + * Used when the execution strategy is not an AsyncExecutionStrategy: simply dispatch always after each DF. + */ +@Internal +public class FallbackDataLoaderDispatchStrategy implements DataLoaderDispatchStrategy { + + private final ExecutionContext executionContext; + + public FallbackDataLoaderDispatchStrategy(ExecutionContext executionContext) { + this.executionContext = executionContext; + } + + + @Override + public DataFetcher modifyDataFetcher(DataFetcher dataFetcher) { + return (DataFetcher) environment -> { + Object obj = dataFetcher.get(environment); + executionContext.getDataLoaderRegistry().dispatchAll(); + return obj; + }; + + } +} diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java new file mode 100644 index 0000000000..95ed19ef53 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -0,0 +1,220 @@ +package graphql.execution.instrumentation.dataloader; + +import graphql.Assert; +import graphql.Internal; +import graphql.execution.DataLoaderDispatchStrategy; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.FieldValueInfo; +import graphql.schema.DataFetcher; +import org.dataloader.DataLoaderRegistry; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +@Internal +public class PerLevelDataLoaderDispatchStrategy implements DataLoaderDispatchStrategy { + + private final CallStack callStack; + private final ExecutionContext executionContext; + + private static class CallStack { + private final LevelMap expectedFetchCountPerLevel = new LevelMap(); + private final LevelMap fetchCountPerLevel = new LevelMap(); + private final LevelMap expectedStrategyCallsPerLevel = new LevelMap(); + private final LevelMap happenedStrategyCallsPerLevel = new LevelMap(); + private final LevelMap happenedOnFieldValueCallsPerLevel = new LevelMap(); + + private final Set dispatchedLevels = new LinkedHashSet<>(); + + public CallStack() { + expectedStrategyCallsPerLevel.set(1, 1); + } + + void increaseExpectedFetchCount(int level, int count) { + expectedFetchCountPerLevel.increment(level, count); + } + + void increaseFetchCount(int level) { + fetchCountPerLevel.increment(level, 1); + } + + void increaseExpectedStrategyCalls(int level, int count) { + expectedStrategyCallsPerLevel.increment(level, count); + } + + void increaseHappenedStrategyCalls(int level) { + happenedStrategyCallsPerLevel.increment(level, 1); + } + + void increaseHappenedOnFieldValueCalls(int level) { + happenedOnFieldValueCallsPerLevel.increment(level, 1); + } + + boolean allStrategyCallsHappened(int level) { + return happenedStrategyCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); + } + + boolean allOnFieldCallsHappened(int level) { + return happenedOnFieldValueCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); + } + + boolean allFetchesHappened(int level) { + return fetchCountPerLevel.get(level) == expectedFetchCountPerLevel.get(level); + } + + @Override + public String toString() { + return "CallStack{" + + "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + + ", fetchCountPerLevel=" + fetchCountPerLevel + + ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + + ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + + ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + + ", dispatchedLevels" + dispatchedLevels + + '}'; + } + + public boolean dispatchIfNotDispatchedBefore(int level) { + if (dispatchedLevels.contains(level)) { + Assert.assertShouldNeverHappen("level " + level + " already dispatched"); + return false; + } + dispatchedLevels.add(level); + return true; + } + } + + public PerLevelDataLoaderDispatchStrategy(ExecutionContext executionContext) { + this.callStack = new CallStack(); + this.executionContext = executionContext; + } + + @Override + public void executionStrategy(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + int curLevel = parameters.getExecutionStepInfo().getPath().getLevel() + 1; + increaseCallCounts(curLevel, parameters); + } + + @Override + public void executionStrategy_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { + int curLevel = parameters.getPath().getLevel() + 1; + onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel); + } + + @Override + public void executeObject_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { + int curLevel = parameters.getPath().getLevel() + 1; + synchronized (callStack) { + callStack.increaseHappenedOnFieldValueCalls(curLevel); + } + } + + + @Override + public void executionStrategy_onFieldValuesException(Throwable t, ExecutionStrategyParameters executionStrategyParameters) { + int curLevel = executionStrategyParameters.getPath().getLevel() + 1; + synchronized (callStack) { + callStack.increaseHappenedOnFieldValueCalls(curLevel); + } + + } + + @Override + public void executeObject_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { + int curLevel = parameters.getPath().getLevel() + 1; + onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel); + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private void increaseCallCounts(int curLevel, ExecutionStrategyParameters executionStrategyParameters) { + int fieldCount = executionStrategyParameters.getFields().size(); + synchronized (callStack) { + callStack.increaseExpectedFetchCount(curLevel, fieldCount); + callStack.increaseHappenedStrategyCalls(curLevel); + } + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private void onFieldValuesInfoDispatchIfNeeded(List fieldValueInfoList, int curLevel) { + boolean dispatchNeeded; + synchronized (callStack) { + dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, curLevel); + } + if (dispatchNeeded) { + dispatch(); + } + } + + // +// thread safety: called with synchronised(callStack) +// + private boolean handleOnFieldValuesInfo(List fieldValueInfos, int curLevel) { + callStack.increaseHappenedOnFieldValueCalls(curLevel); + int expectedStrategyCalls = getCountForList(fieldValueInfos); + callStack.increaseExpectedStrategyCalls(curLevel + 1, expectedStrategyCalls); + return dispatchIfNeeded(curLevel + 1); + } + + private int getCountForList(List fieldValueInfos) { + int result = 0; + for (FieldValueInfo fieldValueInfo : fieldValueInfos) { + if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { + result += 1; + } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { + result += getCountForList(fieldValueInfo.getFieldValueInfos()); + } + } + return result; + } + + + @Override + public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, DataFetcher dataFetcher, CompletableFuture fetchedValue) { + int level = executionStrategyParameters.getPath().getLevel(); + boolean dispatchNeeded; + synchronized (callStack) { + callStack.increaseFetchCount(level); + dispatchNeeded = dispatchIfNeeded(level); + } + if (dispatchNeeded) { + dispatch(); + } + + } + + + // +// thread safety : called with synchronised(callStack) +// + private boolean dispatchIfNeeded(int level) { + if (levelReady(level)) { + return callStack.dispatchIfNotDispatchedBefore(level); + } + return false; + } + + // +// thread safety: called with synchronised(callStack) +// + private boolean levelReady(int level) { + if (level == 1) { + // level 1 is special: there is only one strategy call and that's it + return callStack.allFetchesHappened(1); + } + if (levelReady(level - 1) && callStack.allOnFieldCallsHappened(level - 1) + && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { + return true; + } + return false; + } + + void dispatch() { + DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); + dataLoaderRegistry.dispatchAll(); + } + +} + From b246d1669f75b46291ed48b5ec91ec67060ab30e Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 13 Feb 2024 17:14:41 +1000 Subject: [PATCH 175/393] wip --- src/main/java/graphql/ExecutionInput.java | 4 +- src/main/java/graphql/GraphQL.java | 46 +- .../graphql/execution/ExecutionStrategy.java | 4 +- .../DataLoaderDispatcherInstrumentation.java | 382 ++++++------ ...oaderDispatcherInstrumentationOptions.java | 76 +-- ...aLoaderDispatcherInstrumentationState.java | 180 +++--- .../EmptyDataLoaderRegistryInstance.java | 30 + .../FieldLevelTrackingApproach.java | 570 +++++++++--------- src/test/groovy/example/http/HttpMain.java | 24 +- src/test/groovy/graphql/GraphQLTest.groovy | 65 +- src/test/groovy/graphql/Issue2068.groovy | 14 +- ...ataLoaderCompanyProductMutationTest.groovy | 2 +- ...LoaderDispatcherInstrumentationTest.groovy | 49 +- .../dataloader/DataLoaderHangingTest.groovy | 4 +- .../dataloader/DataLoaderNodeTest.groovy | 2 +- .../DataLoaderPerformanceData.groovy | 7 + .../DataLoaderPerformanceTest.groovy | 4 +- ...manceWithChainedInstrumentationTest.groovy | 10 +- .../DataLoaderTypeMismatchTest.groovy | 2 +- .../Issue1178DataLoaderDispatchTest.groovy | 4 +- ...eCompaniesAndProductsDataLoaderTest.groovy | 3 +- .../readme/DataLoaderBatchingExamples.java | 14 +- 22 files changed, 755 insertions(+), 741 deletions(-) create mode 100644 src/main/java/graphql/execution/instrumentation/dataloader/EmptyDataLoaderRegistryInstance.java diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index bfda46a3d3..08b92a1706 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -3,7 +3,6 @@ import graphql.collect.ImmutableKit; import graphql.execution.ExecutionId; import graphql.execution.RawVariables; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationState; import org.dataloader.DataLoaderRegistry; import java.util.Locale; @@ -12,6 +11,7 @@ import java.util.function.UnaryOperator; import static graphql.Assert.assertNotNull; +import static graphql.execution.instrumentation.dataloader.EmptyDataLoaderRegistryInstance.EMPTY_DATALOADER_REGISTRY; /** * This represents the series of values that can be input on a graphql query execution @@ -213,7 +213,7 @@ public static class Builder { // 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. // - private DataLoaderRegistry dataLoaderRegistry = DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY; + private DataLoaderRegistry dataLoaderRegistry = EMPTY_DATALOADER_REGISTRY; private Locale locale = Locale.getDefault(); private ExecutionId executionId; diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 535accfdbe..90735f7117 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -19,7 +19,6 @@ import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.NoContextChainedInstrumentation; import graphql.execution.instrumentation.SimplePerformantInstrumentation; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; @@ -169,7 +168,6 @@ public ValueUnboxer getValueUnboxer() { * 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) { @@ -181,18 +179,17 @@ public static Builder newGraphQL(GraphQLSchema graphQLSchema) { * 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(this.queryStrategy) - .mutationExecutionStrategy(this.mutationStrategy) - .subscriptionExecutionStrategy(this.subscriptionStrategy) - .executionIdProvider(Optional.ofNullable(this.idProvider).orElse(builder.idProvider)) - .instrumentation(Optional.ofNullable(this.instrumentation).orElse(builder.instrumentation)) - .preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider)); + .queryExecutionStrategy(this.queryStrategy) + .mutationExecutionStrategy(this.mutationStrategy) + .subscriptionExecutionStrategy(this.subscriptionStrategy) + .executionIdProvider(Optional.ofNullable(this.idProvider).orElse(builder.idProvider)) + .instrumentation(Optional.ofNullable(this.instrumentation).orElse(builder.instrumentation)) + .preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider)); builderConsumer.accept(builder); @@ -242,7 +239,6 @@ public Builder subscriptionExecutionStrategy(ExecutionStrategy executionStrategy * in {@link graphql.schema.DataFetcher} invocations. * * @param dataFetcherExceptionHandler the default handler for data fetching exception - * * @return this builder */ public Builder defaultDataFetcherExceptionHandler(DataFetcherExceptionHandler dataFetcherExceptionHandler) { @@ -308,13 +304,12 @@ public GraphQL build() { * 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(); + .query(query) + .build(); return execute(executionInput); } @@ -323,7 +318,6 @@ public ExecutionResult execute(String query) { * 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) { @@ -341,7 +335,6 @@ public ExecutionResult execute(ExecutionInput.Builder executionInputBuilder) { * * * @param builderFunction a function that is given a {@link ExecutionInput.Builder} - * * @return an {@link ExecutionResult} which can include errors */ public ExecutionResult execute(UnaryOperator builderFunction) { @@ -352,7 +345,6 @@ public ExecutionResult execute(UnaryOperator builderFunc * 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) { @@ -374,7 +366,6 @@ public ExecutionResult execute(ExecutionInput executionInput) { * 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) { @@ -395,7 +386,6 @@ public CompletableFuture executeAsync(ExecutionInput.Builder ex * * * @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) { @@ -409,7 +399,6 @@ public CompletableFuture executeAsync(UnaryOperator executeAsync(ExecutionInput executionInput) { @@ -522,7 +511,7 @@ private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchem DocumentAndVariables documentAndVariables = parseResult.getDocumentAndVariables(); documentAndVariables = instrumentation.instrumentDocumentAndVariables(documentAndVariables, parameters, instrumentationState); return ParseAndValidateResult.newResult() - .document(documentAndVariables.getDocument()).variables(documentAndVariables.getVariables()).build(); + .document(documentAndVariables.getDocument()).variables(documentAndVariables.getVariables()).build(); } } @@ -552,14 +541,15 @@ private static Instrumentation checkInstrumentationDefaultState(Instrumentation if (doNotAddDefaultInstrumentations) { return instrumentation == null ? SimplePerformantInstrumentation.INSTANCE : instrumentation; } - if (instrumentation instanceof DataLoaderDispatcherInstrumentation) { - return instrumentation; - } + // if (instrumentation instanceof DataLoaderDispatcherInstrumentation) { + // return instrumentation; + // } if (instrumentation instanceof NoContextChainedInstrumentation) { return instrumentation; } if (instrumentation == null) { - return new DataLoaderDispatcherInstrumentation(); + // return new DataLoaderDispatcherInstrumentation(); + return SimplePerformantInstrumentation.INSTANCE; } // @@ -572,10 +562,10 @@ private static Instrumentation checkInstrumentationDefaultState(Instrumentation } else { instrumentationList.add(instrumentation); } - boolean containsDLInstrumentation = instrumentationList.stream().anyMatch(instr -> instr instanceof DataLoaderDispatcherInstrumentation); - if (!containsDLInstrumentation) { - instrumentationList.add(new DataLoaderDispatcherInstrumentation()); - } + // boolean containsDLInstrumentation = instrumentationList.stream().anyMatch(instr -> instr instanceof DataLoaderDispatcherInstrumentation); + // if (!containsDLInstrumentation) { + // instrumentationList.add(new DataLoaderDispatcherInstrumentation()); + // } return new ChainedInstrumentation(instrumentationList); } } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index c1f4480030..88f4718674 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -17,7 +17,6 @@ import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationState; import graphql.execution.instrumentation.dataloader.FallbackDataLoaderDispatchStrategy; import graphql.execution.instrumentation.dataloader.PerLevelDataLoaderDispatchStrategy; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -68,6 +67,7 @@ import static graphql.execution.FieldValueInfo.CompleteValueType.OBJECT; import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; +import static graphql.execution.instrumentation.dataloader.EmptyDataLoaderRegistryInstance.EMPTY_DATALOADER_REGISTRY; import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment; import static graphql.schema.GraphQLTypeUtil.isEnum; import static graphql.schema.GraphQLTypeUtil.isList; @@ -162,7 +162,7 @@ public void initBeforeExecution(ExecutionContext executionContext) { } private void initDataLoaderStrategy(ExecutionContext executionContext) { - if (executionContext.getDataLoaderRegistry() == DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY) { + if (executionContext.getDataLoaderRegistry() == EMPTY_DATALOADER_REGISTRY) { this.dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; return; } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 208b41a671..6db098064b 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -1,191 +1,191 @@ -package graphql.execution.instrumentation.dataloader; - -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.PublicApi; -import graphql.collect.ImmutableKit; -import graphql.execution.AsyncExecutionStrategy; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStrategy; -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; -import graphql.execution.instrumentation.SimplePerformantInstrumentation; -import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; -import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import graphql.language.OperationDefinition; -import graphql.schema.DataFetcher; -import org.dataloader.DataLoader; -import org.dataloader.DataLoaderRegistry; -import org.dataloader.stats.Statistics; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -import static graphql.execution.instrumentation.InstrumentationState.ofState; -import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; - -/** - * This graphql {@link graphql.execution.instrumentation.Instrumentation} will dispatch - * all the contained {@link org.dataloader.DataLoader}s when each level of the graphql - * query is executed. - *

- * This allows you to use {@link org.dataloader.DataLoader}s in your {@link graphql.schema.DataFetcher}s - * to optimal loading of data. - *

- * A DataLoaderDispatcherInstrumentation will be automatically added to the {@link graphql.GraphQL} - * instrumentation list if one is not present. - * - * @see org.dataloader.DataLoader - * @see org.dataloader.DataLoaderRegistry - */ -@PublicApi -public class DataLoaderDispatcherInstrumentation extends SimplePerformantInstrumentation { - private final DataLoaderDispatcherInstrumentationOptions options; - - /** - * Creates a DataLoaderDispatcherInstrumentation with the default options - */ - public DataLoaderDispatcherInstrumentation() { - this(DataLoaderDispatcherInstrumentationOptions.newOptions()); - } - - /** - * Creates a DataLoaderDispatcherInstrumentation with the specified options - * - * @param options the options to control the behaviour - */ - public DataLoaderDispatcherInstrumentation(DataLoaderDispatcherInstrumentationOptions options) { - this.options = options; - } - - - @Override - public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return new DataLoaderDispatcherInstrumentationState(parameters.getExecutionInput().getDataLoaderRegistry()); - } - - @Override - public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { - DataLoaderDispatcherInstrumentationState state = ofState(rawState); - if (state.isAggressivelyBatching()) { - return dataFetcher; - } - // - // currently only AsyncExecutionStrategy with DataLoader and hence this allows us to "dispatch" - // on every object if it's not using aggressive batching for other execution strategies - // which allows them to work if used. - return (DataFetcher) environment -> { - Object obj = dataFetcher.get(environment); - immediatelyDispatch(state); - return obj; - }; - } - - private void immediatelyDispatch(DataLoaderDispatcherInstrumentationState state) { - state.getApproach().dispatch(); - } - - @Override - public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState rawState) { - DataLoaderDispatcherInstrumentationState state = ofState(rawState); - // - // during #instrumentExecutionInput they could have enhanced the data loader registry - // so we grab it now just before the query operation gets started - // - DataLoaderRegistry finalRegistry = parameters.getExecutionContext().getDataLoaderRegistry(); - state.setDataLoaderRegistry(finalRegistry); - if (!isDataLoaderCompatibleExecution(parameters.getExecutionContext())) { - state.setAggressivelyBatching(false); - } - return noOp(); - } - - private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContext) { - // - // Currently we only support aggressive batching for the AsyncExecutionStrategy. - // This may change in the future but this is the fix for now. - // - OperationDefinition.Operation operation = executionContext.getOperationDefinition().getOperation(); - ExecutionStrategy strategy = executionContext.getStrategy(operation); - return (strategy instanceof AsyncExecutionStrategy); - } - - @Override - public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { - DataLoaderDispatcherInstrumentationState state = ofState(rawState); - // - // if there are no data loaders, there is nothing to do - // - if (state.hasNoDataLoaders()) { - return ExecutionStrategyInstrumentationContext.NOOP; - } - return state.getApproach().beginExecutionStrategy(parameters, state.getState()); - } - - @Override - public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { - DataLoaderDispatcherInstrumentationState state = ofState(rawState); - // - // if there are no data loaders, there is nothing to do - // - if (state.hasNoDataLoaders()) { - return ExecuteObjectInstrumentationContext.NOOP; - } - return state.getApproach().beginObjectResolution(parameters, state.getState()); - } - - @Override - public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { - DataLoaderDispatcherInstrumentationState state = ofState(rawState); - // - // if there are no data loaders, there is nothing to do - // - if (state.hasNoDataLoaders()) { - return noOp(); - } - return state.getApproach().beginFieldFetch(parameters, state.getState()); - } - - @Override - public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState rawState) { - if (!options.isIncludeStatistics()) { - return CompletableFuture.completedFuture(executionResult); - } - DataLoaderDispatcherInstrumentationState state = ofState(rawState); - Map currentExt = executionResult.getExtensions(); - Map statsMap = new LinkedHashMap<>(currentExt == null ? ImmutableKit.emptyMap() : currentExt); - Map dataLoaderStats = buildStatsMap(state); - statsMap.put("dataloader", dataLoaderStats); - - return CompletableFuture.completedFuture(new ExecutionResultImpl(executionResult.getData(), executionResult.getErrors(), statsMap)); - } - - private Map buildStatsMap(DataLoaderDispatcherInstrumentationState state) { - DataLoaderRegistry dataLoaderRegistry = state.getDataLoaderRegistry(); - Statistics allStats = dataLoaderRegistry.getStatistics(); - Map statsMap = new LinkedHashMap<>(); - statsMap.put("overall-statistics", allStats.toMap()); - - Map individualStatsMap = new LinkedHashMap<>(); - - for (String dlKey : dataLoaderRegistry.getKeys()) { - DataLoader dl = dataLoaderRegistry.getDataLoader(dlKey); - Statistics statistics = dl.getStatistics(); - individualStatsMap.put(dlKey, statistics.toMap()); - } - - statsMap.put("individual-statistics", individualStatsMap); - - return statsMap; - } -} +// package graphql.execution.instrumentation.dataloader; +// +// import graphql.ExecutionResult; +// import graphql.ExecutionResultImpl; +// import graphql.PublicApi; +// import graphql.collect.ImmutableKit; +// import graphql.execution.AsyncExecutionStrategy; +// import graphql.execution.ExecutionContext; +// import graphql.execution.ExecutionStrategy; +// import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; +// import graphql.execution.instrumentation.InstrumentationContext; +// import graphql.execution.instrumentation.InstrumentationState; +// import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; +// import graphql.execution.instrumentation.SimplePerformantInstrumentation; +// import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +// import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +// import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +// import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +// import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +// import graphql.language.OperationDefinition; +// import graphql.schema.DataFetcher; +// import org.dataloader.DataLoader; +// import org.dataloader.DataLoaderRegistry; +// import org.dataloader.stats.Statistics; +// import org.jetbrains.annotations.NotNull; +// import org.jetbrains.annotations.Nullable; +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// +// import java.util.LinkedHashMap; +// import java.util.Map; +// import java.util.concurrent.CompletableFuture; +// +// import static graphql.execution.instrumentation.InstrumentationState.ofState; +// import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; +// +// /** +// * This graphql {@link graphql.execution.instrumentation.Instrumentation} will dispatch +// * all the contained {@link org.dataloader.DataLoader}s when each level of the graphql +// * query is executed. +// *

+// * This allows you to use {@link org.dataloader.DataLoader}s in your {@link graphql.schema.DataFetcher}s +// * to optimal loading of data. +// *

+// * A DataLoaderDispatcherInstrumentation will be automatically added to the {@link graphql.GraphQL} +// * instrumentation list if one is not present. +// * +// * @see org.dataloader.DataLoader +// * @see org.dataloader.DataLoaderRegistry +// */ +// @PublicApi +// public class DataLoaderDispatcherInstrumentation extends SimplePerformantInstrumentation { +// private final DataLoaderDispatcherInstrumentationOptions options; +// +// /** +// * Creates a DataLoaderDispatcherInstrumentation with the default options +// */ +// public DataLoaderDispatcherInstrumentation() { +// this(DataLoaderDispatcherInstrumentationOptions.newOptions()); +// } +// +// /** +// * Creates a DataLoaderDispatcherInstrumentation with the specified options +// * +// * @param options the options to control the behaviour +// */ +// public DataLoaderDispatcherInstrumentation(DataLoaderDispatcherInstrumentationOptions options) { +// this.options = options; +// } +// +// +// @Override +// public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { +// return new DataLoaderDispatcherInstrumentationState(parameters.getExecutionInput().getDataLoaderRegistry()); +// } +// +// @Override +// public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { +// DataLoaderDispatcherInstrumentationState state = ofState(rawState); +// if (state.isAggressivelyBatching()) { +// return dataFetcher; +// } +// // +// // currently only AsyncExecutionStrategy with DataLoader and hence this allows us to "dispatch" +// // on every object if it's not using aggressive batching for other execution strategies +// // which allows them to work if used. +// return (DataFetcher) environment -> { +// Object obj = dataFetcher.get(environment); +// immediatelyDispatch(state); +// return obj; +// }; +// } +// +// private void immediatelyDispatch(DataLoaderDispatcherInstrumentationState state) { +// state.getApproach().dispatch(); +// } +// +// @Override +// public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState rawState) { +// DataLoaderDispatcherInstrumentationState state = ofState(rawState); +// // +// // during #instrumentExecutionInput they could have enhanced the data loader registry +// // so we grab it now just before the query operation gets started +// // +// DataLoaderRegistry finalRegistry = parameters.getExecutionContext().getDataLoaderRegistry(); +// state.setDataLoaderRegistry(finalRegistry); +// if (!isDataLoaderCompatibleExecution(parameters.getExecutionContext())) { +// state.setAggressivelyBatching(false); +// } +// return noOp(); +// } +// +// private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContext) { +// // +// // Currently we only support aggressive batching for the AsyncExecutionStrategy. +// // This may change in the future but this is the fix for now. +// // +// OperationDefinition.Operation operation = executionContext.getOperationDefinition().getOperation(); +// ExecutionStrategy strategy = executionContext.getStrategy(operation); +// return (strategy instanceof AsyncExecutionStrategy); +// } +// +// @Override +// public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { +// DataLoaderDispatcherInstrumentationState state = ofState(rawState); +// // +// // if there are no data loaders, there is nothing to do +// // +// if (state.hasNoDataLoaders()) { +// return ExecutionStrategyInstrumentationContext.NOOP; +// } +// return state.getApproach().beginExecutionStrategy(parameters, state.getState()); +// } +// +// @Override +// public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { +// DataLoaderDispatcherInstrumentationState state = ofState(rawState); +// // +// // if there are no data loaders, there is nothing to do +// // +// if (state.hasNoDataLoaders()) { +// return ExecuteObjectInstrumentationContext.NOOP; +// } +// return state.getApproach().beginObjectResolution(parameters, state.getState()); +// } +// +// @Override +// public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { +// DataLoaderDispatcherInstrumentationState state = ofState(rawState); +// // +// // if there are no data loaders, there is nothing to do +// // +// if (state.hasNoDataLoaders()) { +// return noOp(); +// } +// return state.getApproach().beginFieldFetch(parameters, state.getState()); +// } +// +// @Override +// public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState rawState) { +// if (!options.isIncludeStatistics()) { +// return CompletableFuture.completedFuture(executionResult); +// } +// DataLoaderDispatcherInstrumentationState state = ofState(rawState); +// Map currentExt = executionResult.getExtensions(); +// Map statsMap = new LinkedHashMap<>(currentExt == null ? ImmutableKit.emptyMap() : currentExt); +// Map dataLoaderStats = buildStatsMap(state); +// statsMap.put("dataloader", dataLoaderStats); +// +// return CompletableFuture.completedFuture(new ExecutionResultImpl(executionResult.getData(), executionResult.getErrors(), statsMap)); +// } +// +// private Map buildStatsMap(DataLoaderDispatcherInstrumentationState state) { +// DataLoaderRegistry dataLoaderRegistry = state.getDataLoaderRegistry(); +// Statistics allStats = dataLoaderRegistry.getStatistics(); +// Map statsMap = new LinkedHashMap<>(); +// statsMap.put("overall-statistics", allStats.toMap()); +// +// Map individualStatsMap = new LinkedHashMap<>(); +// +// for (String dlKey : dataLoaderRegistry.getKeys()) { +// DataLoader dl = dataLoaderRegistry.getDataLoader(dlKey); +// Statistics statistics = dl.getStatistics(); +// individualStatsMap.put(dlKey, statistics.toMap()); +// } +// +// statsMap.put("individual-statistics", individualStatsMap); +// +// return statsMap; +// } +// } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java index bde9c03bfe..11c2cf1553 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java @@ -1,38 +1,38 @@ -package graphql.execution.instrumentation.dataloader; - -import graphql.PublicApi; - -/** - * The options that control the operation of {@link graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation} - */ -@PublicApi -public class DataLoaderDispatcherInstrumentationOptions { - - private final boolean includeStatistics; - - private DataLoaderDispatcherInstrumentationOptions(boolean includeStatistics) { - this.includeStatistics = includeStatistics; - } - - public static DataLoaderDispatcherInstrumentationOptions newOptions() { - return new DataLoaderDispatcherInstrumentationOptions(false); - } - - /** - * This will toggle the ability to include java-dataloader statistics into the extensions - * output of your query - * - * @param flag the switch to follow - * - * @return a new options object - */ - public DataLoaderDispatcherInstrumentationOptions includeStatistics(boolean flag) { - return new DataLoaderDispatcherInstrumentationOptions(flag); - } - - - public boolean isIncludeStatistics() { - return includeStatistics; - } - -} +// package graphql.execution.instrumentation.dataloader; +// +// import graphql.PublicApi; +// +// /** +// * The options that control the operation of {@link graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation} +// */ +// @PublicApi +// public class DataLoaderDispatcherInstrumentationOptions { +// +// private final boolean includeStatistics; +// +// private DataLoaderDispatcherInstrumentationOptions(boolean includeStatistics) { +// this.includeStatistics = includeStatistics; +// } +// +// public static DataLoaderDispatcherInstrumentationOptions newOptions() { +// return new DataLoaderDispatcherInstrumentationOptions(false); +// } +// +// /** +// * This will toggle the ability to include java-dataloader statistics into the extensions +// * output of your query +// * +// * @param flag the switch to follow +// * +// * @return a new options object +// */ +// public DataLoaderDispatcherInstrumentationOptions includeStatistics(boolean flag) { +// return new DataLoaderDispatcherInstrumentationOptions(flag); +// } +// +// +// public boolean isIncludeStatistics() { +// return includeStatistics; +// } +// +// } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java index 31d1db0499..9336f2c4ad 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java @@ -1,90 +1,90 @@ -package graphql.execution.instrumentation.dataloader; - -import graphql.Assert; -import graphql.Internal; -import graphql.PublicApi; -import graphql.execution.instrumentation.InstrumentationState; -import org.dataloader.DataLoader; -import org.dataloader.DataLoaderRegistry; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -/** - * A base class that keeps track of whether aggressive batching can be used - */ -@PublicApi -public class DataLoaderDispatcherInstrumentationState implements InstrumentationState { - - @Internal - public static final DataLoaderRegistry EMPTY_DATALOADER_REGISTRY = new DataLoaderRegistry() { - - private static final String ERROR_MESSAGE = "You MUST set in your own DataLoaderRegistry to use data loader"; - - @Override - public DataLoaderRegistry register(String key, DataLoader dataLoader) { - return Assert.assertShouldNeverHappen(ERROR_MESSAGE); - } - - @Override - public DataLoader computeIfAbsent(final String key, - final Function> mappingFunction) { - return Assert.assertShouldNeverHappen(ERROR_MESSAGE); - } - - @Override - public DataLoaderRegistry unregister(String key) { - return Assert.assertShouldNeverHappen(ERROR_MESSAGE); - } - }; - - private final FieldLevelTrackingApproach approach; - private final AtomicReference dataLoaderRegistry; - private final InstrumentationState state; - private volatile boolean aggressivelyBatching = true; - private volatile boolean hasNoDataLoaders; - - public DataLoaderDispatcherInstrumentationState(DataLoaderRegistry dataLoaderRegistry) { - this.dataLoaderRegistry = new AtomicReference<>(dataLoaderRegistry); - this.approach = new FieldLevelTrackingApproach(this::getDataLoaderRegistry); - this.state = approach.createState(); - hasNoDataLoaders = checkForNoDataLoader(dataLoaderRegistry); - } - - private boolean checkForNoDataLoader(DataLoaderRegistry dataLoaderRegistry) { - // - // if they have never set a dataloader into the execution input then we can optimize - // away the tracking code - // - return dataLoaderRegistry == EMPTY_DATALOADER_REGISTRY; - } - - boolean isAggressivelyBatching() { - return aggressivelyBatching; - } - - void setAggressivelyBatching(boolean aggressivelyBatching) { - this.aggressivelyBatching = aggressivelyBatching; - } - - FieldLevelTrackingApproach getApproach() { - return approach; - } - - DataLoaderRegistry getDataLoaderRegistry() { - return dataLoaderRegistry.get(); - } - - void setDataLoaderRegistry(DataLoaderRegistry newRegistry) { - dataLoaderRegistry.set(newRegistry); - hasNoDataLoaders = checkForNoDataLoader(newRegistry); - } - - boolean hasNoDataLoaders() { - return hasNoDataLoaders; - } - - InstrumentationState getState() { - return state; - } -} +// package graphql.execution.instrumentation.dataloader; +// +// import graphql.Assert; +// import graphql.Internal; +// import graphql.PublicApi; +// import graphql.execution.instrumentation.InstrumentationState; +// import org.dataloader.DataLoader; +// import org.dataloader.DataLoaderRegistry; +// +// import java.util.concurrent.atomic.AtomicReference; +// import java.util.function.Function; +// +// /** +// * A base class that keeps track of whether aggressive batching can be used +// */ +// @PublicApi +// public class DataLoaderDispatcherInstrumentationState implements InstrumentationState { +// +// @Internal +// public static final DataLoaderRegistry EMPTY_DATALOADER_REGISTRY = new DataLoaderRegistry() { +// +// private static final String ERROR_MESSAGE = "You MUST set in your own DataLoaderRegistry to use data loader"; +// +// @Override +// public DataLoaderRegistry register(String key, DataLoader dataLoader) { +// return Assert.assertShouldNeverHappen(ERROR_MESSAGE); +// } +// +// @Override +// public DataLoader computeIfAbsent(final String key, +// final Function> mappingFunction) { +// return Assert.assertShouldNeverHappen(ERROR_MESSAGE); +// } +// +// @Override +// public DataLoaderRegistry unregister(String key) { +// return Assert.assertShouldNeverHappen(ERROR_MESSAGE); +// } +// }; +// +// private final FieldLevelTrackingApproach approach; +// private final AtomicReference dataLoaderRegistry; +// private final InstrumentationState state; +// private volatile boolean aggressivelyBatching = true; +// private volatile boolean hasNoDataLoaders; +// +// public DataLoaderDispatcherInstrumentationState(DataLoaderRegistry dataLoaderRegistry) { +// this.dataLoaderRegistry = new AtomicReference<>(dataLoaderRegistry); +// this.approach = new FieldLevelTrackingApproach(this::getDataLoaderRegistry); +// this.state = approach.createState(); +// hasNoDataLoaders = checkForNoDataLoader(dataLoaderRegistry); +// } +// +// private boolean checkForNoDataLoader(DataLoaderRegistry dataLoaderRegistry) { +// // +// // if they have never set a dataloader into the execution input then we can optimize +// // away the tracking code +// // +// return dataLoaderRegistry == EMPTY_DATALOADER_REGISTRY; +// } +// +// boolean isAggressivelyBatching() { +// return aggressivelyBatching; +// } +// +// void setAggressivelyBatching(boolean aggressivelyBatching) { +// this.aggressivelyBatching = aggressivelyBatching; +// } +// +// FieldLevelTrackingApproach getApproach() { +// return approach; +// } +// +// DataLoaderRegistry getDataLoaderRegistry() { +// return dataLoaderRegistry.get(); +// } +// +// void setDataLoaderRegistry(DataLoaderRegistry newRegistry) { +// dataLoaderRegistry.set(newRegistry); +// hasNoDataLoaders = checkForNoDataLoader(newRegistry); +// } +// +// boolean hasNoDataLoaders() { +// return hasNoDataLoaders; +// } +// +// InstrumentationState getState() { +// return state; +// } +// } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/EmptyDataLoaderRegistryInstance.java b/src/main/java/graphql/execution/instrumentation/dataloader/EmptyDataLoaderRegistryInstance.java new file mode 100644 index 0000000000..8684ce6c38 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/dataloader/EmptyDataLoaderRegistryInstance.java @@ -0,0 +1,30 @@ +package graphql.execution.instrumentation.dataloader; + +import graphql.Assert; +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderRegistry; + +import java.util.function.Function; + +public class EmptyDataLoaderRegistryInstance { + public static final DataLoaderRegistry EMPTY_DATALOADER_REGISTRY = new DataLoaderRegistry() { + // + private static final String ERROR_MESSAGE = "You MUST set in your own DataLoaderRegistry to use data loader"; + + @Override + public DataLoaderRegistry register(String key, DataLoader dataLoader) { + return Assert.assertShouldNeverHappen(ERROR_MESSAGE); + } + + @Override + public DataLoader computeIfAbsent(final String key, + final Function> mappingFunction) { + return Assert.assertShouldNeverHappen(ERROR_MESSAGE); + } + + @Override + public DataLoaderRegistry unregister(String key) { + return Assert.assertShouldNeverHappen(ERROR_MESSAGE); + } + }; +} diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 93029ee269..2dfe9d5cda 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -1,285 +1,285 @@ -package graphql.execution.instrumentation.dataloader; - -import graphql.Assert; -import graphql.ExecutionResult; -import graphql.Internal; -import graphql.execution.FieldValueInfo; -import graphql.execution.ResultPath; -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import org.dataloader.DataLoaderRegistry; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; - -/** - * This approach uses field level tracking to achieve its aims of making the data loader more efficient - */ -@Internal -public class FieldLevelTrackingApproach { - private final Supplier dataLoaderRegistrySupplier; - private static class CallStack implements InstrumentationState { - - private final LevelMap expectedFetchCountPerLevel = new LevelMap(); - private final LevelMap fetchCountPerLevel = new LevelMap(); - private final LevelMap expectedStrategyCallsPerLevel = new LevelMap(); - private final LevelMap happenedStrategyCallsPerLevel = new LevelMap(); - private final LevelMap happenedOnFieldValueCallsPerLevel = new LevelMap(); - - private final Set dispatchedLevels = new LinkedHashSet<>(); - - CallStack() { - expectedStrategyCallsPerLevel.set(1, 1); - } - - void increaseExpectedFetchCount(int level, int count) { - expectedFetchCountPerLevel.increment(level, count); - } - - void increaseFetchCount(int level) { - fetchCountPerLevel.increment(level, 1); - } - - void increaseExpectedStrategyCalls(int level, int count) { - expectedStrategyCallsPerLevel.increment(level, count); - } - - void increaseHappenedStrategyCalls(int level) { - happenedStrategyCallsPerLevel.increment(level, 1); - } - - void increaseHappenedOnFieldValueCalls(int level) { - happenedOnFieldValueCallsPerLevel.increment(level, 1); - } - - boolean allStrategyCallsHappened(int level) { - return happenedStrategyCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); - } - - boolean allOnFieldCallsHappened(int level) { - return happenedOnFieldValueCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); - } - - boolean allFetchesHappened(int level) { - return fetchCountPerLevel.get(level) == expectedFetchCountPerLevel.get(level); - } - - @Override - public String toString() { - return "CallStack{" + - "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + - ", fetchCountPerLevel=" + fetchCountPerLevel + - ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + - ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + - ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + - ", dispatchedLevels" + dispatchedLevels + - '}'; - } - - public boolean dispatchIfNotDispatchedBefore(int level) { - if (dispatchedLevels.contains(level)) { - Assert.assertShouldNeverHappen("level " + level + " already dispatched"); - return false; - } - dispatchedLevels.add(level); - return true; - } - - public void clearAndMarkCurrentLevelAsReady(int level) { - expectedFetchCountPerLevel.clear(); - fetchCountPerLevel.clear(); - expectedStrategyCallsPerLevel.clear(); - happenedStrategyCallsPerLevel.clear(); - happenedOnFieldValueCallsPerLevel.clear(); - dispatchedLevels.clear(); - - // make sure the level is ready - expectedFetchCountPerLevel.increment(level, 1); - expectedStrategyCallsPerLevel.increment(level, 1); - happenedStrategyCallsPerLevel.increment(level, 1); - } - } - - public FieldLevelTrackingApproach(Supplier dataLoaderRegistrySupplier) { - this.dataLoaderRegistrySupplier = dataLoaderRegistrySupplier; - } - - public InstrumentationState createState() { - return new CallStack(); - } - - ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { - CallStack callStack = (CallStack) rawState; - int curLevel = getCurrentLevel(parameters); - increaseCallCounts(callStack, curLevel, parameters); - - return new ExecutionStrategyInstrumentationContext() { - @Override - public void onDispatched(CompletableFuture result) { - - } - - @Override - public void onCompleted(ExecutionResult result, Throwable t) { - - } - - @Override - public void onFieldValuesInfo(List fieldValueInfoList) { - onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); - } - - @Override - public void onFieldValuesException() { - synchronized (callStack) { - callStack.increaseHappenedOnFieldValueCalls(curLevel); - } - } - }; - } - - ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { - CallStack callStack = (CallStack) rawState; - int curLevel = getCurrentLevel(parameters); - increaseCallCounts(callStack, curLevel, parameters); - - return new ExecuteObjectInstrumentationContext() { - - @Override - public void onDispatched(CompletableFuture> result) { - } - - @Override - public void onCompleted(Map result, Throwable t) { - } - - @Override - public void onFieldValuesInfo(List fieldValueInfoList) { - onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); - } - - @Override - public void onFieldValuesException() { - synchronized (callStack) { - callStack.increaseHappenedOnFieldValueCalls(curLevel); - } - } - }; - } - - private int getCurrentLevel(InstrumentationExecutionStrategyParameters parameters) { - ResultPath path = parameters.getExecutionStrategyParameters().getPath(); - return path.getLevel() + 1; - } - - @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") - private static void increaseCallCounts(CallStack callStack, int curLevel, InstrumentationExecutionStrategyParameters parameters) { - int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); - synchronized (callStack) { - callStack.increaseExpectedFetchCount(curLevel, fieldCount); - callStack.increaseHappenedStrategyCalls(curLevel); - } - } - - @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") - private void onFieldValuesInfoDispatchIfNeeded(CallStack callStack, List fieldValueInfoList, int curLevel) { - boolean dispatchNeeded; - synchronized (callStack) { - dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); - } - if (dispatchNeeded) { - dispatch(); - } - } - - // - // thread safety: called with synchronised(callStack) - // - private boolean handleOnFieldValuesInfo(List fieldValueInfos, CallStack callStack, int curLevel) { - callStack.increaseHappenedOnFieldValueCalls(curLevel); - int expectedStrategyCalls = getCountForList(fieldValueInfos); - callStack.increaseExpectedStrategyCalls(curLevel + 1, expectedStrategyCalls); - return dispatchIfNeeded(callStack, curLevel + 1); - } - - private int getCountForList(List fieldValueInfos) { - int result = 0; - for (FieldValueInfo fieldValueInfo : fieldValueInfos) { - if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { - result += 1; - } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { - result += getCountForList(fieldValueInfo.getFieldValueInfos()); - } - } - return result; - } - - - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { - CallStack callStack = (CallStack) rawState; - ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); - int level = path.getLevel(); - return new InstrumentationContext() { - - @Override - public void onDispatched(CompletableFuture result) { - boolean dispatchNeeded; - synchronized (callStack) { - callStack.increaseFetchCount(level); - dispatchNeeded = dispatchIfNeeded(callStack, level); - } - if (dispatchNeeded) { - dispatch(); - } - - } - - @Override - public void onCompleted(Object result, Throwable t) { - } - }; - } - - - // - // thread safety : called with synchronised(callStack) - // - private boolean dispatchIfNeeded(CallStack callStack, int level) { - if (levelReady(callStack, level)) { - return callStack.dispatchIfNotDispatchedBefore(level); - } - return false; - } - - // - // thread safety: called with synchronised(callStack) - // - private boolean levelReady(CallStack callStack, int level) { - if (level == 1) { - // level 1 is special: there is only one strategy call and that's it - return callStack.allFetchesHappened(1); - } - if (levelReady(callStack, level - 1) && callStack.allOnFieldCallsHappened(level - 1) - && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { - return true; - } - return false; - } - - void dispatch() { - DataLoaderRegistry dataLoaderRegistry = getDataLoaderRegistry(); - dataLoaderRegistry.dispatchAll(); - } - - private DataLoaderRegistry getDataLoaderRegistry() { - return dataLoaderRegistrySupplier.get(); - } -} \ No newline at end of file +// package graphql.execution.instrumentation.dataloader; +// +// import graphql.Assert; +// import graphql.ExecutionResult; +// import graphql.Internal; +// import graphql.execution.FieldValueInfo; +// import graphql.execution.ResultPath; +// import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; +// import graphql.execution.instrumentation.InstrumentationContext; +// import graphql.execution.instrumentation.InstrumentationState; +// import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; +// import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +// import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +// import org.dataloader.DataLoaderRegistry; +// +// import java.util.LinkedHashSet; +// import java.util.List; +// import java.util.Map; +// import java.util.Set; +// import java.util.concurrent.CompletableFuture; +// import java.util.function.Supplier; +// +// /** +// * This approach uses field level tracking to achieve its aims of making the data loader more efficient +// */ +// @Internal +// public class FieldLevelTrackingApproach { +// private final Supplier dataLoaderRegistrySupplier; +// private static class CallStack implements InstrumentationState { +// +// private final LevelMap expectedFetchCountPerLevel = new LevelMap(); +// private final LevelMap fetchCountPerLevel = new LevelMap(); +// private final LevelMap expectedStrategyCallsPerLevel = new LevelMap(); +// private final LevelMap happenedStrategyCallsPerLevel = new LevelMap(); +// private final LevelMap happenedOnFieldValueCallsPerLevel = new LevelMap(); +// +// private final Set dispatchedLevels = new LinkedHashSet<>(); +// +// CallStack() { +// expectedStrategyCallsPerLevel.set(1, 1); +// } +// +// void increaseExpectedFetchCount(int level, int count) { +// expectedFetchCountPerLevel.increment(level, count); +// } +// +// void increaseFetchCount(int level) { +// fetchCountPerLevel.increment(level, 1); +// } +// +// void increaseExpectedStrategyCalls(int level, int count) { +// expectedStrategyCallsPerLevel.increment(level, count); +// } +// +// void increaseHappenedStrategyCalls(int level) { +// happenedStrategyCallsPerLevel.increment(level, 1); +// } +// +// void increaseHappenedOnFieldValueCalls(int level) { +// happenedOnFieldValueCallsPerLevel.increment(level, 1); +// } +// +// boolean allStrategyCallsHappened(int level) { +// return happenedStrategyCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); +// } +// +// boolean allOnFieldCallsHappened(int level) { +// return happenedOnFieldValueCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); +// } +// +// boolean allFetchesHappened(int level) { +// return fetchCountPerLevel.get(level) == expectedFetchCountPerLevel.get(level); +// } +// +// @Override +// public String toString() { +// return "CallStack{" + +// "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + +// ", fetchCountPerLevel=" + fetchCountPerLevel + +// ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + +// ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + +// ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + +// ", dispatchedLevels" + dispatchedLevels + +// '}'; +// } +// +// public boolean dispatchIfNotDispatchedBefore(int level) { +// if (dispatchedLevels.contains(level)) { +// Assert.assertShouldNeverHappen("level " + level + " already dispatched"); +// return false; +// } +// dispatchedLevels.add(level); +// return true; +// } +// +// public void clearAndMarkCurrentLevelAsReady(int level) { +// expectedFetchCountPerLevel.clear(); +// fetchCountPerLevel.clear(); +// expectedStrategyCallsPerLevel.clear(); +// happenedStrategyCallsPerLevel.clear(); +// happenedOnFieldValueCallsPerLevel.clear(); +// dispatchedLevels.clear(); +// +// // make sure the level is ready +// expectedFetchCountPerLevel.increment(level, 1); +// expectedStrategyCallsPerLevel.increment(level, 1); +// happenedStrategyCallsPerLevel.increment(level, 1); +// } +// } +// +// public FieldLevelTrackingApproach(Supplier dataLoaderRegistrySupplier) { +// this.dataLoaderRegistrySupplier = dataLoaderRegistrySupplier; +// } +// +// public InstrumentationState createState() { +// return new CallStack(); +// } +// +// ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { +// CallStack callStack = (CallStack) rawState; +// int curLevel = getCurrentLevel(parameters); +// increaseCallCounts(callStack, curLevel, parameters); +// +// return new ExecutionStrategyInstrumentationContext() { +// @Override +// public void onDispatched(CompletableFuture result) { +// +// } +// +// @Override +// public void onCompleted(ExecutionResult result, Throwable t) { +// +// } +// +// @Override +// public void onFieldValuesInfo(List fieldValueInfoList) { +// onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); +// } +// +// @Override +// public void onFieldValuesException() { +// synchronized (callStack) { +// callStack.increaseHappenedOnFieldValueCalls(curLevel); +// } +// } +// }; +// } +// +// ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { +// CallStack callStack = (CallStack) rawState; +// int curLevel = getCurrentLevel(parameters); +// increaseCallCounts(callStack, curLevel, parameters); +// +// return new ExecuteObjectInstrumentationContext() { +// +// @Override +// public void onDispatched(CompletableFuture> result) { +// } +// +// @Override +// public void onCompleted(Map result, Throwable t) { +// } +// +// @Override +// public void onFieldValuesInfo(List fieldValueInfoList) { +// onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); +// } +// +// @Override +// public void onFieldValuesException() { +// synchronized (callStack) { +// callStack.increaseHappenedOnFieldValueCalls(curLevel); +// } +// } +// }; +// } +// +// private int getCurrentLevel(InstrumentationExecutionStrategyParameters parameters) { +// ResultPath path = parameters.getExecutionStrategyParameters().getPath(); +// return path.getLevel() + 1; +// } +// +// @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") +// private static void increaseCallCounts(CallStack callStack, int curLevel, InstrumentationExecutionStrategyParameters parameters) { +// int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); +// synchronized (callStack) { +// callStack.increaseExpectedFetchCount(curLevel, fieldCount); +// callStack.increaseHappenedStrategyCalls(curLevel); +// } +// } +// +// @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") +// private void onFieldValuesInfoDispatchIfNeeded(CallStack callStack, List fieldValueInfoList, int curLevel) { +// boolean dispatchNeeded; +// synchronized (callStack) { +// dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); +// } +// if (dispatchNeeded) { +// dispatch(); +// } +// } +// +// // +// // thread safety: called with synchronised(callStack) +// // +// private boolean handleOnFieldValuesInfo(List fieldValueInfos, CallStack callStack, int curLevel) { +// callStack.increaseHappenedOnFieldValueCalls(curLevel); +// int expectedStrategyCalls = getCountForList(fieldValueInfos); +// callStack.increaseExpectedStrategyCalls(curLevel + 1, expectedStrategyCalls); +// return dispatchIfNeeded(callStack, curLevel + 1); +// } +// +// private int getCountForList(List fieldValueInfos) { +// int result = 0; +// for (FieldValueInfo fieldValueInfo : fieldValueInfos) { +// if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { +// result += 1; +// } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { +// result += getCountForList(fieldValueInfo.getFieldValueInfos()); +// } +// } +// return result; +// } +// +// +// public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { +// CallStack callStack = (CallStack) rawState; +// ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); +// int level = path.getLevel(); +// return new InstrumentationContext() { +// +// @Override +// public void onDispatched(CompletableFuture result) { +// boolean dispatchNeeded; +// synchronized (callStack) { +// callStack.increaseFetchCount(level); +// dispatchNeeded = dispatchIfNeeded(callStack, level); +// } +// if (dispatchNeeded) { +// dispatch(); +// } +// +// } +// +// @Override +// public void onCompleted(Object result, Throwable t) { +// } +// }; +// } +// +// +// // +// // thread safety : called with synchronised(callStack) +// // +// private boolean dispatchIfNeeded(CallStack callStack, int level) { +// if (levelReady(callStack, level)) { +// return callStack.dispatchIfNotDispatchedBefore(level); +// } +// return false; +// } +// +// // +// // thread safety: called with synchronised(callStack) +// // +// private boolean levelReady(CallStack callStack, int level) { +// if (level == 1) { +// // level 1 is special: there is only one strategy call and that's it +// return callStack.allFetchesHappened(1); +// } +// if (levelReady(callStack, level - 1) && callStack.allOnFieldCallsHappened(level - 1) +// && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { +// return true; +// } +// return false; +// } +// +// void dispatch() { +// DataLoaderRegistry dataLoaderRegistry = getDataLoaderRegistry(); +// dataLoaderRegistry.dispatchAll(); +// } +// +// private DataLoaderRegistry getDataLoaderRegistry() { +// return dataLoaderRegistrySupplier.get(); +// } +// } \ No newline at end of file diff --git a/src/test/groovy/example/http/HttpMain.java b/src/test/groovy/example/http/HttpMain.java index 4f3b7c8936..822352209a 100644 --- a/src/test/groovy/example/http/HttpMain.java +++ b/src/test/groovy/example/http/HttpMain.java @@ -4,10 +4,6 @@ import graphql.ExecutionResult; import graphql.GraphQL; import graphql.StarWarsData; -import graphql.execution.instrumentation.ChainedInstrumentation; -import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; -import graphql.execution.instrumentation.tracing.TracingInstrumentation; import graphql.schema.DataFetcher; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -16,6 +12,9 @@ import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.dataloader.BatchLoader; import org.dataloader.DataLoader; import org.dataloader.DataLoaderFactory; @@ -27,9 +26,6 @@ import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.ResourceHandler; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -42,9 +38,7 @@ import java.util.concurrent.CompletableFuture; import static graphql.ExecutionInput.newExecutionInput; -import static graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions.newOptions; import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; -import static java.util.Arrays.asList; /** * A very simple example of serving a graphql schema over http. @@ -140,18 +134,18 @@ private void handleStarWars(HttpServletRequest httpRequest, HttpServletResponse // you need a schema in order to execute queries GraphQLSchema schema = buildStarWarsSchema(); - DataLoaderDispatcherInstrumentation dlInstrumentation = - new DataLoaderDispatcherInstrumentation(newOptions().includeStatistics(true)); + // DataLoaderDispatcherInstrumentation dlInstrumentation = + // new DataLoaderDispatcherInstrumentation(newOptions().includeStatistics(true)); - Instrumentation instrumentation = new ChainedInstrumentation( - asList(new TracingInstrumentation(), dlInstrumentation) - ); + // Instrumentation instrumentation = new ChainedInstrumentation( + // asList(new TracingInstrumentation(), dlInstrumentation) + // ); // finally you build a runtime graphql object and execute the query GraphQL graphQL = GraphQL .newGraphQL(schema) // instrumentation is pluggable - .instrumentation(instrumentation) + // .instrumentation(instrumentation) .build(); ExecutionResult executionResult = graphQL.execute(executionInput); diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index ec2523d3f8..e183833a97 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -18,7 +18,6 @@ import graphql.execution.ValueUnboxer import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation import graphql.execution.instrumentation.SimplePerformantInstrumentation -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation import graphql.execution.preparsed.NoOpPreparsedDocumentProvider import graphql.language.SourceLocation import graphql.schema.DataFetcher @@ -996,38 +995,38 @@ class GraphQLTest extends Specification { queryStrategy.instrumentation == instrumentation } - def "a single DataLoader instrumentation leaves instrumentation as is"() { - given: - def queryStrategy = new CaptureStrategy() - def instrumentation = new DataLoaderDispatcherInstrumentation() - def builder = GraphQL.newGraphQL(simpleSchema()) - .queryExecutionStrategy(queryStrategy) - .instrumentation(instrumentation) - - when: - def graphql = builder - .build() - graphql.execute('{ hello }') - - then: - queryStrategy.instrumentation == instrumentation - } - - def "DataLoader instrumentation is the default instrumentation"() { - given: - def queryStrategy = new CaptureStrategy() - def builder = GraphQL.newGraphQL(simpleSchema()) - .queryExecutionStrategy(queryStrategy) - - when: - def graphql = builder - .build() - graphql.execute('{ hello }') - - then: - queryStrategy.instrumentation instanceof DataLoaderDispatcherInstrumentation - } - +// def "a single DataLoader instrumentation leaves instrumentation as is"() { +// given: +// def queryStrategy = new CaptureStrategy() +//// def instrumentation = new DataLoaderDispatcherInstrumentation() +// def builder = GraphQL.newGraphQL(simpleSchema()) +// .queryExecutionStrategy(queryStrategy) +// .instrumentation(instrumentation) +// +// when: +// def graphql = builder +// .build() +// graphql.execute('{ hello }') +// +// then: +// queryStrategy.instrumentation == instrumentation +// } + +// def "DataLoader instrumentation is the default instrumentation"() { +// given: +// def queryStrategy = new CaptureStrategy() +// def builder = GraphQL.newGraphQL(simpleSchema()) +// .queryExecutionStrategy(queryStrategy) +// +// when: +// def graphql = builder +// .build() +// graphql.execute('{ hello }') +// +// then: +// queryStrategy.instrumentation instanceof DataLoaderDispatcherInstrumentation +// } +// def "query with triple quoted multi line strings"() { given: def queryType = "Query" diff --git a/src/test/groovy/graphql/Issue2068.groovy b/src/test/groovy/graphql/Issue2068.groovy index a111e425d1..0e83eee201 100644 --- a/src/test/groovy/graphql/Issue2068.groovy +++ b/src/test/groovy/graphql/Issue2068.groovy @@ -1,7 +1,6 @@ package graphql -import graphql.execution.instrumentation.ChainedInstrumentation -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation + import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import graphql.schema.StaticDataFetcher @@ -12,7 +11,6 @@ import org.dataloader.DataLoader import org.dataloader.DataLoaderOptions import org.dataloader.DataLoaderRegistry import spock.lang.Specification -import spock.lang.Timeout import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionStage @@ -21,8 +19,8 @@ import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit -import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring import static graphql.ExecutionInput.newExecutionInput +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring class Issue2068 extends Specification { def "shouldn't hang on exception in resolveFieldWithInfo"() { @@ -95,7 +93,7 @@ class Issue2068 extends Specification { when: def graphql = GraphQL.newGraphQL(schema) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() DataLoaderRegistry dataLoaderRegistry = mkNewDataLoaderRegistry(executor) @@ -127,9 +125,9 @@ class Issue2068 extends Specification { when: graphql = GraphQL.newGraphQL(schema) - .instrumentation(new ChainedInstrumentation( - Collections.singletonList(new DataLoaderDispatcherInstrumentation())) - ) +// .instrumentation(new ChainedInstrumentation( +// Collections.singletonList(new DataLoaderDispatcherInstrumentation())) +// ) .build() graphql.execute(newExecutionInput() diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy index 77e87642f6..e05c36f233 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy @@ -66,7 +66,7 @@ class DataLoaderCompanyProductMutationTest extends Specification { def graphQL = TestUtil.graphQL(spec, wiring) .queryExecutionStrategy(queryES) .mutationExecutionStrategy(mutationES) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() ExecutionInput executionInput = ExecutionInput.newExecutionInput() diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy index ca01f1b3d9..4b7512f538 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy @@ -65,20 +65,20 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { ] - def "dataloader instrumentation is always added and an empty data loader registry is in place"() { - - def captureStrategy = new CaptureStrategy() - def graphQL = GraphQL.newGraphQL(starWarsSchema).queryExecutionStrategy(captureStrategy) - .instrumentation(new SimplePerformantInstrumentation()) - .build() - def executionInput = newExecutionInput().query('{ hero { name } }').build() - when: - graphQL.execute(executionInput) - then: - executionInput.getDataLoaderRegistry() != null - def chainedInstrumentation = captureStrategy.instrumentation as ChainedInstrumentation - chainedInstrumentation.instrumentations.any { instr -> instr instanceof DataLoaderDispatcherInstrumentation } - } +// def "dataloader instrumentation is always added and an empty data loader registry is in place"() { +// +// def captureStrategy = new CaptureStrategy() +// def graphQL = GraphQL.newGraphQL(starWarsSchema).queryExecutionStrategy(captureStrategy) +// .instrumentation(new SimplePerformantInstrumentation()) +// .build() +// def executionInput = newExecutionInput().query('{ hero { name } }').build() +// when: +// graphQL.execute(executionInput) +// then: +// executionInput.getDataLoaderRegistry() != null +// def chainedInstrumentation = captureStrategy.instrumentation as ChainedInstrumentation +//// chainedInstrumentation.instrumentations.any { instr -> instr instanceof DataLoaderDispatcherInstrumentation } +// } def "dispatch is never called if data loader registry is not set"() { def dataLoaderRegistry = new DataLoaderRegistry() { @@ -131,7 +131,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { DataLoaderRegistry startingDataLoaderRegistry = new DataLoaderRegistry() def enhancedDataLoaderRegistry = starWarsWiring.newDataLoaderRegistry() - def dlInstrumentation = new DataLoaderDispatcherInstrumentation() +// def dlInstrumentation = new DataLoaderDispatcherInstrumentation() def enhancingInstrumentation = new SimplePerformantInstrumentation() { @NotNull @@ -142,7 +142,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { } } - def chainedInstrumentation = new ChainedInstrumentation([dlInstrumentation, enhancingInstrumentation]) + def chainedInstrumentation = new ChainedInstrumentation([enhancingInstrumentation]) def graphql = GraphQL.newGraphQL(starWarsWiring.schema) .instrumentation(chainedInstrumentation).build() @@ -165,11 +165,12 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { def starWarsWiring = new StarWarsDataLoaderWiring() def dlRegistry = starWarsWiring.newDataLoaderRegistry() - def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() +// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() def graphql = GraphQL.newGraphQL(starWarsWiring.schema) .queryExecutionStrategy(executionStrategy) - .instrumentation(batchingInstrumentation).build() +// .instrumentation(batchingInstrumentation) + .build() when: @@ -191,7 +192,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { given: def starWarsWiring = new StarWarsDataLoaderWiring() def dlRegistry = starWarsWiring.newDataLoaderRegistry() - def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() +// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() def graphql = GraphQL.newGraphQL(starWarsWiring.schema).instrumentation(batchingInstrumentation).build() @@ -229,9 +230,11 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { given: def starWarsWiring = new StarWarsDataLoaderWiring() def dlRegistry = starWarsWiring.newDataLoaderRegistry() - def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() +// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() - def graphql = GraphQL.newGraphQL(starWarsWiring.schema).instrumentation(batchingInstrumentation).build() + def graphql = GraphQL.newGraphQL(starWarsWiring.schema) +// .instrumentation(batchingInstrumentation) + .build() when: def query = """ @@ -302,9 +305,9 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { given: def support = new DeepDataFetchers() def dummyDataloaderRegistry = new DataLoaderRegistry() - def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() +// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() def graphql = GraphQL.newGraphQL(support.schema()) - .instrumentation(batchingInstrumentation) +// .instrumentation(batchingInstrumentation) .build() // FieldLevelTrackingApproach uses LevelMaps with a default size of 16. // Use a value greater than 16 to ensure that the underlying LevelMaps are resized diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy index cda31ba34b..24b4a78434 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy @@ -125,7 +125,7 @@ class DataLoaderHangingTest extends Specification { when: def graphql = GraphQL.newGraphQL(schema) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() then: "execution shouldn't hang" @@ -353,7 +353,7 @@ class DataLoaderHangingTest extends Specification { GraphQL graphQL = GraphQL .newGraphQL(graphQLSchema) .queryExecutionStrategy(new AsyncExecutionStrategy(customExceptionHandlerThatThrows)) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() when: diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy index 0bfab06b4f..dd4be355f7 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy @@ -135,7 +135,7 @@ class DataLoaderNodeTest extends Specification { DataLoaderRegistry registry = new DataLoaderRegistry().register(childNodesFieldName, loader) ExecutionResult result = GraphQL.newGraphQL(schema) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() .execute(ExecutionInput.newExecutionInput().dataLoaderRegistry(registry).query( ''' diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy index 7ca398f084..bf6064c1b2 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy @@ -28,6 +28,13 @@ class DataLoaderPerformanceData { .build() } + GraphQL setupGraphQL() { + GraphQLSchema schema = new BatchCompare().buildDataLoaderSchema(batchCompareDataFetchers) + + GraphQL.newGraphQL(schema) + .build() + } + static def expectedData = [ shops: [ [id : "shop-1", name: "Shop 1", diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index 4565db0615..158715b8b2 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -1,9 +1,7 @@ package graphql.execution.instrumentation.dataloader - import graphql.ExecutionInput import graphql.GraphQL -import graphql.execution.instrumentation.Instrumentation import org.dataloader.DataLoaderRegistry import spock.lang.Specification @@ -22,7 +20,7 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers = new BatchCompareDataFetchers() DataLoaderPerformanceData dataLoaderPerformanceData = new DataLoaderPerformanceData(batchCompareDataFetchers) dataLoaderRegistry = dataLoaderPerformanceData.setupDataLoaderRegistry() - Instrumentation instrumentation = new DataLoaderDispatcherInstrumentation() +// Instrumentation instrumentation = new DataLoaderDispatcherInstrumentation() graphQL = dataLoaderPerformanceData.setupGraphQL(instrumentation) } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 7b1bd96d54..e675950ab6 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -1,10 +1,7 @@ package graphql.execution.instrumentation.dataloader - import graphql.ExecutionInput import graphql.GraphQL -import graphql.execution.instrumentation.ChainedInstrumentation -import graphql.execution.instrumentation.Instrumentation import org.dataloader.DataLoaderRegistry import spock.lang.Ignore import spock.lang.Specification @@ -26,9 +23,10 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification DataLoaderPerformanceData dataLoaderPerformanceData = new DataLoaderPerformanceData(batchCompareDataFetchers) dataLoaderRegistry = dataLoaderPerformanceData.setupDataLoaderRegistry() - Instrumentation instrumentation = new ChainedInstrumentation( - Collections.singletonList(new DataLoaderDispatcherInstrumentation())) - graphQL = dataLoaderPerformanceData.setupGraphQL(instrumentation) +// Instrumentation instrumentation = new ChainedInstrumentation( +// Collections.singletonList(new DataLoaderDispatcherInstrumentation()) +// ) + graphQL = dataLoaderPerformanceData.setupGraphQL() } def "chainedInstrumentation: 760 ensure data loader is performant for lists"() { diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy index 79ea8e2a49..4ffd8c1199 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy @@ -62,7 +62,7 @@ class DataLoaderTypeMismatchTest extends Specification { def schema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring) def graphql = GraphQL.newGraphQL(schema) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() when: diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy index 1c206692bc..ac8472830c 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy @@ -75,8 +75,8 @@ class Issue1178DataLoaderDispatchTest extends Specification { when: def graphql = TestUtil.graphQL(sdl, wiring) - .instrumentation(new DataLoaderDispatcherInstrumentation()) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() then: "execution shouldn't error" diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy index 1b36ec4ecb..2bbaa27a9c 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy @@ -18,7 +18,6 @@ import spock.lang.Specification import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionStage import java.util.stream.Collectors -import java.util.stream.IntStream import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring @@ -184,7 +183,7 @@ class PeopleCompaniesAndProductsDataLoaderTest extends Specification { GraphQL graphQL = GraphQL .newGraphQL(graphQLSchema) - .instrumentation(new DataLoaderDispatcherInstrumentation()) +// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() when: diff --git a/src/test/groovy/readme/DataLoaderBatchingExamples.java b/src/test/groovy/readme/DataLoaderBatchingExamples.java index 8d13a4581f..24f5e1e7db 100644 --- a/src/test/groovy/readme/DataLoaderBatchingExamples.java +++ b/src/test/groovy/readme/DataLoaderBatchingExamples.java @@ -3,8 +3,6 @@ import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; -import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import graphql.schema.GraphQLSchema; @@ -85,12 +83,12 @@ public Object get(DataFetchingEnvironment environment) { // available to the query and the associated DataFetchers // // In this case we use options to make it keep statistics on the batching efficiency + // // + // DataLoaderDispatcherInstrumentationOptions options = DataLoaderDispatcherInstrumentationOptions + // .newOptions().includeStatistics(true); // - DataLoaderDispatcherInstrumentationOptions options = DataLoaderDispatcherInstrumentationOptions - .newOptions().includeStatistics(true); - - DataLoaderDispatcherInstrumentation dispatcherInstrumentation - = new DataLoaderDispatcherInstrumentation(options); + // DataLoaderDispatcherInstrumentation dispatcherInstrumentation + // = new DataLoaderDispatcherInstrumentation(options); // // now build your graphql object and execute queries on it. @@ -98,7 +96,7 @@ public Object get(DataFetchingEnvironment environment) { // schema fields // GraphQL graphQL = GraphQL.newGraphQL(buildSchema()) - .instrumentation(dispatcherInstrumentation) + // .instrumentation(dispatcherInstrumentation) .build(); // From 98574e7056bbe8832dd65e965a45fa48321b11cf Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 13 Feb 2024 19:55:12 +1000 Subject: [PATCH 176/393] wip --- src/main/java/graphql/GraphQL.java | 47 +++++++------------ .../java/graphql/execution/Execution.java | 4 +- .../graphql/execution/ExecutionStrategy.java | 2 +- .../instrumentation/dataloader/LevelMap.java | 11 +++++ .../PerLevelDataLoaderDispatchStrategy.java | 17 +++++-- src/test/groovy/graphql/Issue2068.groovy | 4 -- ...ataLoaderCompanyProductMutationTest.groovy | 1 - ...LoaderDispatcherInstrumentationTest.groovy | 9 +--- 8 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 90735f7117..1419dbdd4e 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -12,7 +12,6 @@ import graphql.execution.SimpleDataFetcherExceptionHandler; import graphql.execution.SubscriptionExecutionStrategy; import graphql.execution.ValueUnboxer; -import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.DocumentAndVariables; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; @@ -29,7 +28,6 @@ import graphql.schema.GraphQLSchema; import graphql.validation.ValidationError; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -207,6 +205,7 @@ public static class Builder { private Instrumentation instrumentation = null; // deliberate default here private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; private boolean doNotAddDefaultInstrumentations = false; + private boolean doNotAutomaticallyDispatchDataLoader = false; private ValueUnboxer valueUnboxer = ValueUnboxer.DEFAULT; @@ -262,19 +261,28 @@ public Builder executionIdProvider(ExecutionIdProvider executionIdProvider) { } /** - * For performance reasons you can opt into situation where the default instrumentations (such - * as {@link graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation} will not be - * automatically added into the graphql instance. + * As of GraphQL Java 22 there are no default instrumentations. Before that a DataLoaderDispatcherInstrumentation + * was added by default. *

- * For most situations this is not needed unless you are really pushing the boundaries of performance - *

- * By default a certain graphql instrumentations will be added to the mix to more easily enable certain functionality. This - * allows you to stop this behavior + * For GraphQL Java 22 calling this method is equal to calling {@link #doNotAutomaticallyDispatchDataLoader()} * * @return this builder */ public Builder doNotAddDefaultInstrumentations() { this.doNotAddDefaultInstrumentations = true; + // legacy reasons: calling this method is equal to calling doNotAutomaticallyDispatchDataLoader + this.doNotAutomaticallyDispatchDataLoader = true; + return this; + } + + /** + * Deactivates the automatic dispatching of DataLoaders. + * If deactivated the user is responsible for dispatching the DataLoaders manually. + * + * @return this builder + */ + public Builder doNotAutomaticallyDispatchDataLoader() { + this.doNotAutomaticallyDispatchDataLoader = true; return this; } @@ -541,31 +549,12 @@ private static Instrumentation checkInstrumentationDefaultState(Instrumentation if (doNotAddDefaultInstrumentations) { return instrumentation == null ? SimplePerformantInstrumentation.INSTANCE : instrumentation; } - // if (instrumentation instanceof DataLoaderDispatcherInstrumentation) { - // return instrumentation; - // } if (instrumentation instanceof NoContextChainedInstrumentation) { return instrumentation; } if (instrumentation == null) { - // return new DataLoaderDispatcherInstrumentation(); return SimplePerformantInstrumentation.INSTANCE; } - - // - // if we don't have a DataLoaderDispatcherInstrumentation in play, we add one. We want DataLoader to be 1st class in graphql without requiring - // people to remember to wire it in. Later we may decide to have more default instrumentations but for now it's just the one - // - List instrumentationList = new ArrayList<>(); - if (instrumentation instanceof ChainedInstrumentation) { - instrumentationList.addAll(((ChainedInstrumentation) instrumentation).getInstrumentations()); - } else { - instrumentationList.add(instrumentation); - } - // boolean containsDLInstrumentation = instrumentationList.stream().anyMatch(instr -> instr instanceof DataLoaderDispatcherInstrumentation); - // if (!containsDLInstrumentation) { - // instrumentationList.add(new DataLoaderDispatcherInstrumentation()); - // } - return new ChainedInstrumentation(instrumentationList); + return instrumentation; } } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index ac38a276cf..2472292311 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -148,10 +148,12 @@ private CompletableFuture executeOperation(ExecutionContext exe .path(path) .build(); + executionContext.getQueryStrategy().initBeforeExecution(executionContext); + executionContext.getMutationStrategy().initBeforeExecution(executionContext); + executionContext.getSubscriptionStrategy().initBeforeExecution(executionContext); CompletableFuture result; try { ExecutionStrategy executionStrategy = executionContext.getStrategy(operation); - executionStrategy.initBeforeExecution(executionContext); result = executionStrategy.execute(executionContext, parameters); } catch (NonNullableFieldWasNullException e) { // this means it was non-null types all the way from an offending non-null type diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 88f4718674..5f669b2709 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -136,7 +136,7 @@ public abstract class ExecutionStrategy { protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; private final ResolveType resolvedType = new ResolveType(); - protected DataLoaderDispatchStrategy dataLoaderDispatcherStrategy; + protected DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; /** * The default execution strategy constructor uses the {@link SimpleDataFetcherExceptionHandler} diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/LevelMap.java b/src/main/java/graphql/execution/instrumentation/dataloader/LevelMap.java index 9e151ab40b..ddf46f643b 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/LevelMap.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/LevelMap.java @@ -1,6 +1,7 @@ package graphql.execution.instrumentation.dataloader; import graphql.Internal; + import java.util.Arrays; /** @@ -62,6 +63,16 @@ public String toString() { return result.toString(); } + public String toString(int level) { + StringBuilder result = new StringBuilder(); + result.append("IntMap["); + for (int i = 1; i <= level; i++) { + result.append("level=").append(i).append(",count=").append(countsByLevel[i]).append(" "); + } + result.append("]"); + return result.toString(); + } + public void clear() { Arrays.fill(countsByLevel, 0); } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index 95ed19ef53..b2e2839eed 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -77,6 +77,7 @@ public String toString() { '}'; } + public boolean dispatchIfNotDispatchedBefore(int level) { if (dispatchedLevels.contains(level)) { Assert.assertShouldNeverHappen("level " + level + " already dispatched"); @@ -104,6 +105,12 @@ public void executionStrategy_onFieldValuesInfo(List fieldValueI onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel); } + @Override + public void executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + int curLevel = parameters.getExecutionStepInfo().getPath().getLevel() + 1; + increaseCallCounts(curLevel, parameters); + } + @Override public void executeObject_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { int curLevel = parameters.getPath().getLevel() + 1; @@ -144,7 +151,7 @@ private void onFieldValuesInfoDispatchIfNeeded(List fieldValueIn dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, curLevel); } if (dispatchNeeded) { - dispatch(); + dispatch(curLevel); } } @@ -180,7 +187,7 @@ public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyPar dispatchNeeded = dispatchIfNeeded(level); } if (dispatchNeeded) { - dispatch(); + dispatch(level); } } @@ -190,7 +197,8 @@ public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyPar // thread safety : called with synchronised(callStack) // private boolean dispatchIfNeeded(int level) { - if (levelReady(level)) { + boolean ready = levelReady(level); + if (ready) { return callStack.dispatchIfNotDispatchedBefore(level); } return false; @@ -206,12 +214,13 @@ private boolean levelReady(int level) { } if (levelReady(level - 1) && callStack.allOnFieldCallsHappened(level - 1) && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { + return true; } return false; } - void dispatch() { + void dispatch(int level) { DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); dataLoaderRegistry.dispatchAll(); } diff --git a/src/test/groovy/graphql/Issue2068.groovy b/src/test/groovy/graphql/Issue2068.groovy index 0e83eee201..3273eab2cf 100644 --- a/src/test/groovy/graphql/Issue2068.groovy +++ b/src/test/groovy/graphql/Issue2068.groovy @@ -93,7 +93,6 @@ class Issue2068 extends Specification { when: def graphql = GraphQL.newGraphQL(schema) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() DataLoaderRegistry dataLoaderRegistry = mkNewDataLoaderRegistry(executor) @@ -125,9 +124,6 @@ class Issue2068 extends Specification { when: graphql = GraphQL.newGraphQL(schema) -// .instrumentation(new ChainedInstrumentation( -// Collections.singletonList(new DataLoaderDispatcherInstrumentation())) -// ) .build() graphql.execute(newExecutionInput() diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy index e05c36f233..649da5e0d4 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductMutationTest.groovy @@ -66,7 +66,6 @@ class DataLoaderCompanyProductMutationTest extends Specification { def graphQL = TestUtil.graphQL(spec, wiring) .queryExecutionStrategy(queryES) .mutationExecutionStrategy(mutationES) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() ExecutionInput executionInput = ExecutionInput.newExecutionInput() diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy index 4b7512f538..be55b2984a 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy @@ -187,14 +187,13 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { "AsyncSerialExecutionStrategy" | new AsyncSerialExecutionStrategy() || _ } - def "basic batch loading is possible via instrumentation interception of Execution Strategies"() { + def "basic batch loading is possible"() { given: def starWarsWiring = new StarWarsDataLoaderWiring() def dlRegistry = starWarsWiring.newDataLoaderRegistry() -// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() - def graphql = GraphQL.newGraphQL(starWarsWiring.schema).instrumentation(batchingInstrumentation).build() + def graphql = GraphQL.newGraphQL(starWarsWiring.schema).build() when: @@ -230,10 +229,8 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { given: def starWarsWiring = new StarWarsDataLoaderWiring() def dlRegistry = starWarsWiring.newDataLoaderRegistry() -// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() def graphql = GraphQL.newGraphQL(starWarsWiring.schema) -// .instrumentation(batchingInstrumentation) .build() when: @@ -305,9 +302,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { given: def support = new DeepDataFetchers() def dummyDataloaderRegistry = new DataLoaderRegistry() -// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() def graphql = GraphQL.newGraphQL(support.schema()) -// .instrumentation(batchingInstrumentation) .build() // FieldLevelTrackingApproach uses LevelMaps with a default size of 16. // Use a value greater than 16 to ensure that the underlying LevelMaps are resized From 95b8b412e685e8346f603d59ef5a09cb4ffd8195 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 13 Feb 2024 20:22:38 +1000 Subject: [PATCH 177/393] wip --- src/main/java/graphql/GraphQL.java | 14 ++- .../AsyncSerialExecutionStrategy.java | 5 +- .../java/graphql/execution/Execution.java | 105 +++++++++++------- .../graphql/execution/ExecutionStrategy.java | 20 +--- src/test/groovy/graphql/GraphQLTest.groovy | 13 +-- .../graphql/execution/ExecutionTest.groovy | 21 ++-- .../dataloader/DataLoaderHangingTest.groovy | 1 - 7 files changed, 100 insertions(+), 79 deletions(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 1419dbdd4e..a83f9ea74d 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -93,6 +93,7 @@ public class GraphQL { private final Instrumentation instrumentation; private final PreparsedDocumentProvider preparsedDocumentProvider; private final ValueUnboxer valueUnboxer; + private final boolean doNotAutomaticallyDispatchDataLoader; private GraphQL(Builder builder) { @@ -104,6 +105,7 @@ private GraphQL(Builder builder) { this.instrumentation = assertNotNull(builder.instrumentation, () -> "instrumentation must not be null"); this.preparsedDocumentProvider = assertNotNull(builder.preparsedDocumentProvider, () -> "preparsedDocumentProvider must be non null"); this.valueUnboxer = assertNotNull(builder.valueUnboxer, () -> "valueUnboxer must not be null"); + this.doNotAutomaticallyDispatchDataLoader = builder.doNotAutomaticallyDispatchDataLoader; } /** @@ -148,6 +150,10 @@ public Instrumentation getInstrumentation() { return instrumentation; } + public boolean isDoNotAutomaticallyDispatchDataLoader() { + return doNotAutomaticallyDispatchDataLoader; + } + /** * @return the PreparsedDocumentProvider for this {@link GraphQL} instance */ @@ -537,9 +543,13 @@ private List validate(ExecutionInput executionInput, Document d return validationErrors; } - private CompletableFuture execute(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { + private CompletableFuture execute(ExecutionInput executionInput, + Document document, + GraphQLSchema graphQLSchema, + InstrumentationState instrumentationState + ) { - Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer); + Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, doNotAutomaticallyDispatchDataLoader); ExecutionId executionId = executionInput.getExecutionId(); return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState); diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index 1c0e4b3924..212b201e37 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -30,11 +30,12 @@ public AsyncSerialExecutionStrategy(DataFetcherExceptionHandler exceptionHandler @Override @SuppressWarnings({"TypeParameterUnusedInFormals", "FutureReturnValueIgnored"}) public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + dataLoaderDispatcherStrategy.executionStrategy(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); InstrumentationContext executionStrategyCtx = nonNullCtx(instrumentation.beginExecutionStrategy(instrumentationParameters, - executionContext.getInstrumentationState()) + executionContext.getInstrumentationState()) ); MergedSelectionSet fields = parameters.getFields(); ImmutableList fieldNames = ImmutableList.copyOf(fields.keySet()); @@ -43,7 +44,7 @@ public CompletableFuture execute(ExecutionContext executionCont MergedField currentField = fields.getSubField(fieldName); ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); ExecutionStrategyParameters newParameters = parameters - .transform(builder -> builder.field(currentField).path(fieldPath)); + .transform(builder -> builder.field(currentField).path(fieldPath)); return resolveField(executionContext, newParameters); }); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 2472292311..7bfe38552e 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -10,6 +10,8 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.dataloader.FallbackDataLoaderDispatchStrategy; +import graphql.execution.instrumentation.dataloader.PerLevelDataLoaderDispatchStrategy; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.extensions.ExtensionsBuilder; @@ -31,6 +33,7 @@ import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo; import static graphql.execution.ExecutionStrategyParameters.newParameters; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; +import static graphql.execution.instrumentation.dataloader.EmptyDataLoaderRegistryInstance.EMPTY_DATALOADER_REGISTRY; import static java.util.concurrent.CompletableFuture.completedFuture; @Internal @@ -41,13 +44,20 @@ public class Execution { private final ExecutionStrategy subscriptionStrategy; private final Instrumentation instrumentation; private final ValueUnboxer valueUnboxer; - - public Execution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy, Instrumentation instrumentation, ValueUnboxer valueUnboxer) { + private final boolean doNotAutomaticallyDispatchDataLoader; + + public Execution(ExecutionStrategy queryStrategy, + ExecutionStrategy mutationStrategy, + ExecutionStrategy subscriptionStrategy, + Instrumentation instrumentation, + ValueUnboxer valueUnboxer, + boolean doNotAutomaticallyDispatchDataLoader) { this.queryStrategy = queryStrategy != null ? queryStrategy : new AsyncExecutionStrategy(); this.mutationStrategy = mutationStrategy != null ? mutationStrategy : new AsyncSerialExecutionStrategy(); this.subscriptionStrategy = subscriptionStrategy != null ? subscriptionStrategy : new AsyncExecutionStrategy(); this.instrumentation = instrumentation; this.valueUnboxer = valueUnboxer; + this.doNotAutomaticallyDispatchDataLoader = doNotAutomaticallyDispatchDataLoader; } public CompletableFuture execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput, InstrumentationState instrumentationState) { @@ -70,30 +80,30 @@ public CompletableFuture execute(Document document, GraphQLSche } ExecutionContext executionContext = newExecutionContextBuilder() - .instrumentation(instrumentation) - .instrumentationState(instrumentationState) - .executionId(executionId) - .graphQLSchema(graphQLSchema) - .queryStrategy(queryStrategy) - .mutationStrategy(mutationStrategy) - .subscriptionStrategy(subscriptionStrategy) - .context(executionInput.getContext()) - .graphQLContext(executionInput.getGraphQLContext()) - .localContext(executionInput.getLocalContext()) - .root(executionInput.getRoot()) - .fragmentsByName(fragmentsByName) - .coercedVariables(coercedVariables) - .document(document) - .operationDefinition(operationDefinition) - .dataLoaderRegistry(executionInput.getDataLoaderRegistry()) - .locale(executionInput.getLocale()) - .valueUnboxer(valueUnboxer) - .executionInput(executionInput) - .build(); + .instrumentation(instrumentation) + .instrumentationState(instrumentationState) + .executionId(executionId) + .graphQLSchema(graphQLSchema) + .queryStrategy(queryStrategy) + .mutationStrategy(mutationStrategy) + .subscriptionStrategy(subscriptionStrategy) + .context(executionInput.getContext()) + .graphQLContext(executionInput.getGraphQLContext()) + .localContext(executionInput.getLocalContext()) + .root(executionInput.getRoot()) + .fragmentsByName(fragmentsByName) + .coercedVariables(coercedVariables) + .document(document) + .operationDefinition(operationDefinition) + .dataLoaderRegistry(executionInput.getDataLoaderRegistry()) + .locale(executionInput.getLocale()) + .valueUnboxer(valueUnboxer) + .executionInput(executionInput) + .build(); InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters( - executionInput, graphQLSchema, instrumentationState + executionInput, graphQLSchema, instrumentationState ); executionContext = instrumentation.instrumentExecutionContext(executionContext, parameters, instrumentationState); return executeOperation(executionContext, executionInput.getRoot(), executionContext.getOperationDefinition()); @@ -126,12 +136,12 @@ private CompletableFuture executeOperation(ExecutionContext exe } FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() - .schema(executionContext.getGraphQLSchema()) - .objectType(operationRootType) - .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getCoercedVariables().toMap()) - .graphQLContext(graphQLContext) - .build(); + .schema(executionContext.getGraphQLSchema()) + .objectType(operationRootType) + .fragments(executionContext.getFragmentsByName()) + .variables(executionContext.getCoercedVariables().toMap()) + .graphQLContext(graphQLContext) + .build(); MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDefinition.getSelectionSet()); @@ -140,20 +150,23 @@ private CompletableFuture executeOperation(ExecutionContext exe NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo); ExecutionStrategyParameters parameters = newParameters() - .executionStepInfo(executionStepInfo) - .source(root) - .localContext(executionContext.getLocalContext()) - .fields(fields) - .nonNullFieldValidator(nonNullableFieldValidator) - .path(path) - .build(); - - executionContext.getQueryStrategy().initBeforeExecution(executionContext); - executionContext.getMutationStrategy().initBeforeExecution(executionContext); - executionContext.getSubscriptionStrategy().initBeforeExecution(executionContext); + .executionStepInfo(executionStepInfo) + .source(root) + .localContext(executionContext.getLocalContext()) + .fields(fields) + .nonNullFieldValidator(nonNullableFieldValidator) + .path(path) + .build(); + + CompletableFuture result; try { ExecutionStrategy executionStrategy = executionContext.getStrategy(operation); + DataLoaderDispatchStrategy dataLoaderDispatchStrategy = createDataLoaderDispatchStrategy(executionContext, executionStrategy); + executionContext.getQueryStrategy().setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); + executionContext.getMutationStrategy().setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); + executionContext.getSubscriptionStrategy().setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); + result = executionStrategy.execute(executionContext, parameters); } catch (NonNullableFieldWasNullException e) { // this means it was non-null types all the way from an offending non-null type @@ -176,6 +189,18 @@ private CompletableFuture executeOperation(ExecutionContext exe return result; } + private DataLoaderDispatchStrategy createDataLoaderDispatchStrategy(ExecutionContext executionContext, ExecutionStrategy executionStrategy) { + if (executionContext.getDataLoaderRegistry() == EMPTY_DATALOADER_REGISTRY || doNotAutomaticallyDispatchDataLoader) { + return DataLoaderDispatchStrategy.NO_OP; + } + if (executionStrategy instanceof AsyncExecutionStrategy) { + return new PerLevelDataLoaderDispatchStrategy(executionContext); + } else { + return new FallbackDataLoaderDispatchStrategy(executionContext); + } + } + + private void addExtensionsBuilderNotPresent(GraphQLContext graphQLContext) { Object builder = graphQLContext.get(ExtensionsBuilder.class); if (builder == null) { diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5f669b2709..fa6f9046d5 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -17,8 +17,6 @@ import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.dataloader.FallbackDataLoaderDispatchStrategy; -import graphql.execution.instrumentation.dataloader.PerLevelDataLoaderDispatchStrategy; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; @@ -67,7 +65,6 @@ import static graphql.execution.FieldValueInfo.CompleteValueType.OBJECT; import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; -import static graphql.execution.instrumentation.dataloader.EmptyDataLoaderRegistryInstance.EMPTY_DATALOADER_REGISTRY; import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment; import static graphql.schema.GraphQLTypeUtil.isEnum; import static graphql.schema.GraphQLTypeUtil.isList; @@ -157,23 +154,14 @@ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHand this.dataFetcherExceptionHandler = dataFetcherExceptionHandler; } - public void initBeforeExecution(ExecutionContext executionContext) { - initDataLoaderStrategy(executionContext); - } - private void initDataLoaderStrategy(ExecutionContext executionContext) { - if (executionContext.getDataLoaderRegistry() == EMPTY_DATALOADER_REGISTRY) { - this.dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; - return; - } - if (this instanceof AsyncExecutionStrategy) { - this.dataLoaderDispatcherStrategy = new PerLevelDataLoaderDispatchStrategy(executionContext); - } else { - this.dataLoaderDispatcherStrategy = new FallbackDataLoaderDispatchStrategy(executionContext); - } + @Internal + void setDataLoaderDispatcherStrategy(DataLoaderDispatchStrategy dataLoaderDispatcherStrategy) { + this.dataLoaderDispatcherStrategy = dataLoaderDispatcherStrategy; } + @Internal public static String mkNameForPath(Field currentField) { return mkNameForPath(Collections.singletonList(currentField)); diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index e183833a97..c3a5e2c7ff 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -15,7 +15,6 @@ import graphql.execution.ExecutionStrategyParameters import graphql.execution.MissingRootTypeException import graphql.execution.SubscriptionExecutionStrategy import graphql.execution.ValueUnboxer -import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.preparsed.NoOpPreparsedDocumentProvider @@ -949,8 +948,8 @@ class GraphQLTest extends Specification { then: result == [hello: 'world'] queryStrategy.executionId == hello - queryStrategy.instrumentation instanceof ChainedInstrumentation - (queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(instrumentation) + queryStrategy.instrumentation instanceof Instrumentation + queryStrategy.instrumentation == instrumentation when: @@ -972,9 +971,9 @@ class GraphQLTest extends Specification { then: result == [hello: 'world'] queryStrategy.executionId == goodbye - queryStrategy.instrumentation instanceof ChainedInstrumentation - (queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(newInstrumentation) - !(queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(instrumentation) + queryStrategy.instrumentation instanceof Instrumentation +// (queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(newInstrumentation) +// !(queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(instrumentation) } def "disabling data loader instrumentation leaves instrumentation as is"() { @@ -1423,7 +1422,7 @@ many lines'''] graphQL.getIdProvider() == ExecutionIdProvider.DEFAULT_EXECUTION_ID_PROVIDER graphQL.getValueUnboxer() == ValueUnboxer.DEFAULT graphQL.getPreparsedDocumentProvider() == NoOpPreparsedDocumentProvider.INSTANCE - graphQL.getInstrumentation() instanceof ChainedInstrumentation + graphQL.getInstrumentation() instanceof Instrumentation graphQL.getQueryStrategy() instanceof AsyncExecutionStrategy graphQL.getMutationStrategy() instanceof AsyncSerialExecutionStrategy graphQL.getSubscriptionStrategy() instanceof SubscriptionExecutionStrategy diff --git a/src/test/groovy/graphql/execution/ExecutionTest.groovy b/src/test/groovy/graphql/execution/ExecutionTest.groovy index 0cc8f1d287..7130beca0f 100644 --- a/src/test/groovy/graphql/execution/ExecutionTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionTest.groovy @@ -8,7 +8,6 @@ import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.parser.Parser -import org.jetbrains.annotations.NotNull import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -36,7 +35,7 @@ class ExecutionTest extends Specification { def subscriptionStrategy = new CountingExecutionStrategy() def mutationStrategy = new CountingExecutionStrategy() def queryStrategy = new CountingExecutionStrategy() - def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, SimplePerformantInstrumentation.INSTANCE, ValueUnboxer.DEFAULT) + def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, SimplePerformantInstrumentation.INSTANCE, ValueUnboxer.DEFAULT, false) def emptyExecutionInput = ExecutionInput.newExecutionInput().query("query").build() def instrumentationState = new InstrumentationState() {} @@ -114,18 +113,18 @@ class ExecutionTest extends Specification { def instrumentation = new SimplePerformantInstrumentation() { - @Override + @Override ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { - - return ExecutionContextBuilder.newExecutionContextBuilder(executionContext) - .queryStrategy(queryStrategyUpdatedToDuringExecutionContextInstrument) - .build() - } - } - - def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, ValueUnboxer.DEFAULT) + + return ExecutionContextBuilder.newExecutionContextBuilder(executionContext) + .queryStrategy(queryStrategyUpdatedToDuringExecutionContextInstrument) + .build() + } + } + + def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, ValueUnboxer.DEFAULT, false) when: diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy index 24b4a78434..76bc6e2bdf 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy @@ -353,7 +353,6 @@ class DataLoaderHangingTest extends Specification { GraphQL graphQL = GraphQL .newGraphQL(graphQLSchema) .queryExecutionStrategy(new AsyncExecutionStrategy(customExceptionHandlerThatThrows)) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() when: From 2017326e54a46dd939f012e5e5a7961039470d5a Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 13 Feb 2024 20:26:39 +1000 Subject: [PATCH 178/393] wip --- .../instrumentation/dataloader/DataLoaderHangingTest.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy index 76bc6e2bdf..2d98da377f 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy @@ -125,7 +125,6 @@ class DataLoaderHangingTest extends Specification { when: def graphql = GraphQL.newGraphQL(schema) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() then: "execution shouldn't hang" From 4b04397242b1194ee5bc29f68941e854e28969eb Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Tue, 13 Feb 2024 10:25:39 -0800 Subject: [PATCH 179/393] ensure that instrumentations get an executionid --- src/main/java/graphql/GraphQL.java | 2 +- src/test/groovy/graphql/GraphQLTest.groovy | 23 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 535accfdbe..590b252eb8 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -415,7 +415,7 @@ public CompletableFuture executeAsync(UnaryOperator executeAsync(ExecutionInput executionInput) { ExecutionInput executionInputWithId = ensureInputHasId(executionInput); - CompletableFuture instrumentationStateCF = instrumentation.createStateAsync(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInput)); + CompletableFuture instrumentationStateCF = instrumentation.createStateAsync(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInputWithId)); return Async.orNullCompletedFuture(instrumentationStateCF).thenCompose(instrumentationState -> { try { InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInputWithId, this.graphQLSchema, instrumentationState); diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index ec2523d3f8..d0d1f1b636 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -17,8 +17,11 @@ import graphql.execution.SubscriptionExecutionStrategy import graphql.execution.ValueUnboxer import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimpleInstrumentation import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import graphql.execution.preparsed.NoOpPreparsedDocumentProvider import graphql.language.SourceLocation import graphql.schema.DataFetcher @@ -1063,6 +1066,26 @@ over many lines'''] } + def "executionId is set before being passed to instrumentation"() { + InstrumentationCreateStateParameters seenParams + + def instrumentation = new Instrumentation() { + @Override InstrumentationState createState(InstrumentationCreateStateParameters params) { + seenParams = params + null + } + } + + when: + GraphQL.newGraphQL(StarWarsSchema.starWarsSchema) + .instrumentation(instrumentation) + .build() + .execute("{ __typename }") + + then: + seenParams.executionInput.executionId != null + } + def "variables map can't be null via ExecutionInput"() { given: From f796ab7c9b8f8af20e562650576f3e3ff7f8ef2e Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 14 Feb 2024 11:37:22 +1000 Subject: [PATCH 180/393] fix tests, make dataloader strategy per execution --- .../execution/AsyncExecutionStrategy.java | 6 ++-- .../AsyncSerialExecutionStrategy.java | 2 +- .../java/graphql/execution/Execution.java | 5 +--- .../graphql/execution/ExecutionContext.java | 13 +++++++++ .../graphql/execution/ExecutionStrategy.java | 17 ++++------- .../PerLevelDataLoaderDispatchStrategy.java | 28 +++++++++---------- .../DataLoaderPerformanceTest.groovy | 3 +- .../FieldValidationTest.groovy | 5 ++-- 8 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 4397fbfc99..eb505581a2 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -36,7 +36,7 @@ public AsyncExecutionStrategy(DataFetcherExceptionHandler exceptionHandler) { @Override @SuppressWarnings("FutureReturnValueIgnored") public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { - dataLoaderDispatcherStrategy.executionStrategy(executionContext, parameters); + executionContext.getDataLoaderDispatcherStrategy().executionStrategy(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); @@ -59,14 +59,14 @@ public CompletableFuture execute(ExecutionContext executionCont for (FieldValueInfo completeValueInfo : completeValueInfos) { fieldValuesFutures.add(completeValueInfo.getFieldValueFuture()); } - dataLoaderDispatcherStrategy.executionStrategy_onFieldValuesInfo(completeValueInfos, parameters); + executionContext.getDataLoaderDispatcherStrategy().executionStrategy_onFieldValuesInfo(completeValueInfos, parameters); executionStrategyCtx.onFieldValuesInfo(completeValueInfos); fieldValuesFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. - dataLoaderDispatcherStrategy.executeObject_onFieldValuesException(ex, parameters); + executionContext.getDataLoaderDispatcherStrategy().executionStrategy_onFieldValuesException(ex, parameters); executionStrategyCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index 212b201e37..d009b99e4c 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -30,7 +30,7 @@ public AsyncSerialExecutionStrategy(DataFetcherExceptionHandler exceptionHandler @Override @SuppressWarnings({"TypeParameterUnusedInFormals", "FutureReturnValueIgnored"}) public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { - dataLoaderDispatcherStrategy.executionStrategy(executionContext, parameters); + executionContext.getDataLoaderDispatcherStrategy().executionStrategy(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 7bfe38552e..0fb09f7392 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -163,10 +163,7 @@ private CompletableFuture executeOperation(ExecutionContext exe try { ExecutionStrategy executionStrategy = executionContext.getStrategy(operation); DataLoaderDispatchStrategy dataLoaderDispatchStrategy = createDataLoaderDispatchStrategy(executionContext, executionStrategy); - executionContext.getQueryStrategy().setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); - executionContext.getMutationStrategy().setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); - executionContext.getSubscriptionStrategy().setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); - + executionContext.setDataLoaderDispatcherStrategy(dataLoaderDispatchStrategy); result = executionStrategy.execute(executionContext, parameters); } catch (NonNullableFieldWasNullException e) { // this means it was non-null types all the way from an offending non-null type diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 747f955d1a..f4ba52890c 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -6,6 +6,7 @@ import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.GraphQLError; +import graphql.Internal; import graphql.PublicApi; import graphql.collect.ImmutableKit; import graphql.execution.instrumentation.Instrumentation; @@ -57,6 +58,8 @@ public class ExecutionContext { private final ExecutionInput executionInput; private final Supplier queryTree; + private final AtomicReference dataLoaderDispatcherStrategy = new AtomicReference<>(DataLoaderDispatchStrategy.NO_OP); + ExecutionContext(ExecutionContextBuilder builder) { this.graphQLSchema = builder.graphQLSchema; this.executionId = builder.executionId; @@ -269,6 +272,16 @@ public Supplier getNormalizedQueryTree() { return queryTree; } + @Internal + public void setDataLoaderDispatcherStrategy(DataLoaderDispatchStrategy dataLoaderDispatcherStrategy) { + this.dataLoaderDispatcherStrategy.set(dataLoaderDispatcherStrategy); + } + + @Internal + public DataLoaderDispatchStrategy getDataLoaderDispatcherStrategy() { + return dataLoaderDispatcherStrategy.get(); + } + /** * This helps you transform the current ExecutionContext object into another one by starting a builder with all * the current values and allows you to transform it how you want. diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index fa6f9046d5..2b0cd77580 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -133,7 +133,6 @@ public abstract class ExecutionStrategy { protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; private final ResolveType resolvedType = new ResolveType(); - protected DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; /** * The default execution strategy constructor uses the {@link SimpleDataFetcherExceptionHandler} @@ -155,12 +154,6 @@ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHand } - @Internal - void setDataLoaderDispatcherStrategy(DataLoaderDispatchStrategy dataLoaderDispatcherStrategy) { - this.dataLoaderDispatcherStrategy = dataLoaderDispatcherStrategy; - } - - @Internal public static String mkNameForPath(Field currentField) { @@ -197,7 +190,7 @@ public static String mkNameForPath(List currentField) { * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ protected CompletableFuture> executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { - dataLoaderDispatcherStrategy.executeObject(executionContext, parameters); + executionContext.getDataLoaderDispatcherStrategy().executeObject(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); @@ -221,14 +214,14 @@ protected CompletableFuture> executeObject(ExecutionContext for (FieldValueInfo completeValueInfo : completeValueInfos) { resultFutures.add(completeValueInfo.getFieldValueFuture()); } - dataLoaderDispatcherStrategy.executeObject_onFieldValuesInfo(completeValueInfos, parameters); + executionContext.getDataLoaderDispatcherStrategy().executeObject_onFieldValuesInfo(completeValueInfos, parameters); resolveObjectCtx.onFieldValuesInfo(completeValueInfos); resultFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. - dataLoaderDispatcherStrategy.executeObject_onFieldValuesException(ex, parameters); + executionContext.getDataLoaderDispatcherStrategy().executeObject_onFieldValuesException(ex, parameters); resolveObjectCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; @@ -383,10 +376,10 @@ protected CompletableFuture fetchField(ExecutionContext executionC executionContext.getInstrumentationState()) ); - dataFetcher = dataLoaderDispatcherStrategy.modifyDataFetcher(dataFetcher); + dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); - dataLoaderDispatcherStrategy.fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); + executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); fetchCtx.onDispatched(fetchedValue); return fetchedValue .handle((result, exception) -> { diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index b2e2839eed..d629134739 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -102,9 +102,17 @@ public void executionStrategy(ExecutionContext executionContext, ExecutionStrate @Override public void executionStrategy_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { int curLevel = parameters.getPath().getLevel() + 1; - onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel); + onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel, parameters); } + public void executionStrategy_onFieldValuesException(Throwable t, ExecutionStrategyParameters executionStrategyParameters) { + int curLevel = executionStrategyParameters.getPath().getLevel() + 1; + synchronized (callStack) { + callStack.increaseHappenedOnFieldValueCalls(curLevel); + } + } + + @Override public void executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { int curLevel = parameters.getExecutionStepInfo().getPath().getLevel() + 1; @@ -112,28 +120,20 @@ public void executeObject(ExecutionContext executionContext, ExecutionStrategyPa } @Override - public void executeObject_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { + public void executeObject_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { int curLevel = parameters.getPath().getLevel() + 1; - synchronized (callStack) { - callStack.increaseHappenedOnFieldValueCalls(curLevel); - } + onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel, parameters); } @Override - public void executionStrategy_onFieldValuesException(Throwable t, ExecutionStrategyParameters executionStrategyParameters) { - int curLevel = executionStrategyParameters.getPath().getLevel() + 1; + public void executeObject_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { + int curLevel = parameters.getPath().getLevel() + 1; synchronized (callStack) { callStack.increaseHappenedOnFieldValueCalls(curLevel); } - } - @Override - public void executeObject_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { - int curLevel = parameters.getPath().getLevel() + 1; - onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel); - } @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") private void increaseCallCounts(int curLevel, ExecutionStrategyParameters executionStrategyParameters) { @@ -145,7 +145,7 @@ private void increaseCallCounts(int curLevel, ExecutionStrategyParameters execut } @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") - private void onFieldValuesInfoDispatchIfNeeded(List fieldValueInfoList, int curLevel) { + private void onFieldValuesInfoDispatchIfNeeded(List fieldValueInfoList, int curLevel, ExecutionStrategyParameters parameters) { boolean dispatchNeeded; synchronized (callStack) { dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, curLevel); diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy index 158715b8b2..e382353ace 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceTest.groovy @@ -20,8 +20,7 @@ class DataLoaderPerformanceTest extends Specification { batchCompareDataFetchers = new BatchCompareDataFetchers() DataLoaderPerformanceData dataLoaderPerformanceData = new DataLoaderPerformanceData(batchCompareDataFetchers) dataLoaderRegistry = dataLoaderPerformanceData.setupDataLoaderRegistry() -// Instrumentation instrumentation = new DataLoaderDispatcherInstrumentation() - graphQL = dataLoaderPerformanceData.setupGraphQL(instrumentation) + graphQL = dataLoaderPerformanceData.setupGraphQL() } def "760 ensure data loader is performant for lists"() { diff --git a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy index 06034eb2c3..14bc0908cb 100644 --- a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy @@ -12,7 +12,6 @@ import graphql.execution.ExecutionId import graphql.execution.ResultPath import graphql.execution.ValueUnboxer import graphql.execution.instrumentation.ChainedInstrumentation -import graphql.execution.instrumentation.Instrumentation import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import spock.lang.Specification @@ -308,7 +307,7 @@ class FieldValidationTest extends Specification { def document = TestUtil.parseQuery(query) def strategy = new AsyncExecutionStrategy() def instrumentation = new FieldValidationInstrumentation(validation) - def execution = new Execution(strategy, strategy, strategy, instrumentation, ValueUnboxer.DEFAULT) + def execution = new Execution(strategy, strategy, strategy, instrumentation, ValueUnboxer.DEFAULT, false) def executionInput = ExecutionInput.newExecutionInput().query(query).variables(variables).build() execution.execute(document, schema, ExecutionId.generate(), executionInput, SimplePerformantInstrumentation.INSTANCE.createState(new InstrumentationCreateStateParameters(schema, executionInput))) @@ -322,7 +321,7 @@ class FieldValidationTest extends Specification { } } def instrumentations = [new FieldValidationInstrumentation - (fieldValidation)] + (fieldValidation)] def chainedInstrumentation = new ChainedInstrumentation(instrumentations); def graphql = GraphQL .newGraphQL(schema) From fc0a73ed11a764dfdd71565fb8a86c6164fb92cc Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 14 Feb 2024 12:49:09 +1100 Subject: [PATCH 181/393] Fix bug where deferred payloads were not using field aliases --- .../incremental/DeferredExecutionSupport.java | 2 +- .../incremental/DeferredFragmentCall.java | 8 ++-- ...eferExecutionSupportIntegrationTest.groovy | 38 +++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java index 1689afa684..c2701741c2 100644 --- a/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java +++ b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java @@ -165,7 +165,7 @@ private Supplier - new DeferredFragmentCall.FieldWithExecutionResult(currentField.getName(), executionResult) + new DeferredFragmentCall.FieldWithExecutionResult(currentField.getResultKey(), executionResult) ); } ) diff --git a/src/main/java/graphql/execution/incremental/DeferredFragmentCall.java b/src/main/java/graphql/execution/incremental/DeferredFragmentCall.java index be28fb9e90..79b9496fd1 100644 --- a/src/main/java/graphql/execution/incremental/DeferredFragmentCall.java +++ b/src/main/java/graphql/execution/incremental/DeferredFragmentCall.java @@ -107,7 +107,7 @@ private DeferPayload transformToDeferredPayload(List f ImmutableList.Builder errorsBuilder = ImmutableList.builder(); fieldWithExecutionResults.forEach(entry -> { - dataMap.put(entry.fieldName, entry.executionResult.getData()); + dataMap.put(entry.resultKey, entry.executionResult.getData()); errorsBuilder.addAll(entry.executionResult.getErrors()); }); @@ -120,11 +120,11 @@ private DeferPayload transformToDeferredPayload(List f } public static class FieldWithExecutionResult { - private final String fieldName; + private final String resultKey; private final ExecutionResult executionResult; - public FieldWithExecutionResult(String fieldName, ExecutionResult executionResult) { - this.fieldName = fieldName; + public FieldWithExecutionResult(String resultKey, ExecutionResult executionResult) { + this.resultKey = resultKey; this.executionResult = executionResult; } diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index c805132853..62459b52f2 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -201,6 +201,44 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "defer with aliased fields"() { + def query = ''' + query { + postAlias: post { + idAlias: id + ... @defer { + summaryAlias: summary + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [postAlias: [idAlias: "1001"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : false, + incremental: [ + [ + path: ["postAlias"], + data: [summaryAlias: "A summary"] + ] + ] + ] + ] + } + def "defer on interface field"() { def query = """ query { From b708b84391c2282f937dd9ee866967afbc0354fc Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 14 Feb 2024 12:05:24 +1000 Subject: [PATCH 182/393] fix tests --- .../dataloader/PerLevelDataLoaderDispatchStrategy.java | 2 +- .../instrumentation/dataloader/DataLoaderPerformanceData.groovy | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index 3674812e4d..4e2b82623d 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -96,7 +96,7 @@ public PerLevelDataLoaderDispatchStrategy(ExecutionContext executionContext) { @Override public void deferredField(ExecutionContext executionContext, MergedField currentField) { - throw new UnsupportedOperationException("Data Loaders cannot be used to resolve deferred fields at the moment"); + throw new UnsupportedOperationException("Data Loaders cannot be used to resolve deferred fields"); } @Override diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy index cf404b25d5..6246f883d8 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceData.groovy @@ -36,6 +36,7 @@ class DataLoaderPerformanceData { GraphQL setupGraphQL() { GraphQLSchema schema = new BatchCompare().buildDataLoaderSchema(batchCompareDataFetchers) + schema = schema.transform({ bldr -> bldr.additionalDirective(Directives.DeferDirective) }) GraphQL.newGraphQL(schema) .build() From fd082b08c92363d7a2e83349588322c8b272a5bc Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 14 Feb 2024 12:11:39 +1000 Subject: [PATCH 183/393] cleanup --- .../DataLoaderDispatcherInstrumentation.java | 398 ------------ ...oaderDispatcherInstrumentationOptions.java | 38 -- ...aLoaderDispatcherInstrumentationState.java | 90 --- .../FieldLevelTrackingApproach.java | 572 ------------------ 4 files changed, 1098 deletions(-) delete mode 100644 src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java delete mode 100644 src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java delete mode 100644 src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java delete mode 100644 src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java deleted file mode 100644 index 8948599321..0000000000 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ /dev/null @@ -1,398 +0,0 @@ -// <<<<<<< HEAD -// // package graphql.execution.instrumentation.dataloader; -// // -// // import graphql.ExecutionResult; -// // import graphql.ExecutionResultImpl; -// // import graphql.PublicApi; -// // import graphql.collect.ImmutableKit; -// // import graphql.execution.AsyncExecutionStrategy; -// // import graphql.execution.ExecutionContext; -// // import graphql.execution.ExecutionStrategy; -// // import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -// // import graphql.execution.instrumentation.InstrumentationContext; -// // import graphql.execution.instrumentation.InstrumentationState; -// // import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; -// // import graphql.execution.instrumentation.SimplePerformantInstrumentation; -// // import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; -// // import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; -// // import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; -// // import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -// // import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -// // import graphql.language.OperationDefinition; -// // import graphql.schema.DataFetcher; -// // import org.dataloader.DataLoader; -// // import org.dataloader.DataLoaderRegistry; -// // import org.dataloader.stats.Statistics; -// // import org.jetbrains.annotations.NotNull; -// // import org.jetbrains.annotations.Nullable; -// // import org.slf4j.Logger; -// // import org.slf4j.LoggerFactory; -// // -// // import java.util.LinkedHashMap; -// // import java.util.Map; -// // import java.util.concurrent.CompletableFuture; -// // -// // import static graphql.execution.instrumentation.InstrumentationState.ofState; -// // import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; -// // -// // /** -// // * This graphql {@link graphql.execution.instrumentation.Instrumentation} will dispatch -// // * all the contained {@link org.dataloader.DataLoader}s when each level of the graphql -// // * query is executed. -// // *

-// // * This allows you to use {@link org.dataloader.DataLoader}s in your {@link graphql.schema.DataFetcher}s -// // * to optimal loading of data. -// // *

-// // * A DataLoaderDispatcherInstrumentation will be automatically added to the {@link graphql.GraphQL} -// // * instrumentation list if one is not present. -// // * -// // * @see org.dataloader.DataLoader -// // * @see org.dataloader.DataLoaderRegistry -// // */ -// // @PublicApi -// // public class DataLoaderDispatcherInstrumentation extends SimplePerformantInstrumentation { -// // private final DataLoaderDispatcherInstrumentationOptions options; -// // -// // /** -// // * Creates a DataLoaderDispatcherInstrumentation with the default options -// // */ -// // public DataLoaderDispatcherInstrumentation() { -// // this(DataLoaderDispatcherInstrumentationOptions.newOptions()); -// // } -// // -// // /** -// // * Creates a DataLoaderDispatcherInstrumentation with the specified options -// // * -// // * @param options the options to control the behaviour -// // */ -// // public DataLoaderDispatcherInstrumentation(DataLoaderDispatcherInstrumentationOptions options) { -// // this.options = options; -// // } -// // -// // -// // @Override -// // public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { -// // return new DataLoaderDispatcherInstrumentationState(parameters.getExecutionInput().getDataLoaderRegistry()); -// // } -// // -// // @Override -// // public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { -// // DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // if (state.isAggressivelyBatching()) { -// // return dataFetcher; -// // } -// // // -// // // currently only AsyncExecutionStrategy with DataLoader and hence this allows us to "dispatch" -// // // on every object if it's not using aggressive batching for other execution strategies -// // // which allows them to work if used. -// // return (DataFetcher) environment -> { -// // Object obj = dataFetcher.get(environment); -// // immediatelyDispatch(state); -// // return obj; -// // }; -// // } -// // -// // private void immediatelyDispatch(DataLoaderDispatcherInstrumentationState state) { -// // state.getApproach().dispatch(); -// // } -// // -// // @Override -// // public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState rawState) { -// // DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // // -// // // during #instrumentExecutionInput they could have enhanced the data loader registry -// // // so we grab it now just before the query operation gets started -// // // -// // DataLoaderRegistry finalRegistry = parameters.getExecutionContext().getDataLoaderRegistry(); -// // state.setDataLoaderRegistry(finalRegistry); -// // if (!isDataLoaderCompatibleExecution(parameters.getExecutionContext())) { -// // state.setAggressivelyBatching(false); -// // } -// // return noOp(); -// // } -// // -// // private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContext) { -// // // -// // // Currently we only support aggressive batching for the AsyncExecutionStrategy. -// // // This may change in the future but this is the fix for now. -// // // -// // OperationDefinition.Operation operation = executionContext.getOperationDefinition().getOperation(); -// // ExecutionStrategy strategy = executionContext.getStrategy(operation); -// // return (strategy instanceof AsyncExecutionStrategy); -// // } -// // -// // @Override -// // public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// // DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // // -// // // if there are no data loaders, there is nothing to do -// // // -// // if (state.hasNoDataLoaders()) { -// // return ExecutionStrategyInstrumentationContext.NOOP; -// // } -// // return state.getApproach().beginExecutionStrategy(parameters, state.getState()); -// // } -// // -// // @Override -// // public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// // DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // // -// // // if there are no data loaders, there is nothing to do -// // // -// // if (state.hasNoDataLoaders()) { -// // return ExecuteObjectInstrumentationContext.NOOP; -// // } -// // return state.getApproach().beginObjectResolution(parameters, state.getState()); -// // } -// // -// // @Override -// // public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { -// // DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // // -// // // if there are no data loaders, there is nothing to do -// // // -// // if (state.hasNoDataLoaders()) { -// // return noOp(); -// // } -// // return state.getApproach().beginFieldFetch(parameters, state.getState()); -// // } -// // -// // @Override -// // public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState rawState) { -// // if (!options.isIncludeStatistics()) { -// // return CompletableFuture.completedFuture(executionResult); -// // } -// // DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // Map currentExt = executionResult.getExtensions(); -// // Map statsMap = new LinkedHashMap<>(currentExt == null ? ImmutableKit.emptyMap() : currentExt); -// // Map dataLoaderStats = buildStatsMap(state); -// // statsMap.put("dataloader", dataLoaderStats); -// // -// // return CompletableFuture.completedFuture(new ExecutionResultImpl(executionResult.getData(), executionResult.getErrors(), statsMap)); -// // } -// // -// // private Map buildStatsMap(DataLoaderDispatcherInstrumentationState state) { -// // DataLoaderRegistry dataLoaderRegistry = state.getDataLoaderRegistry(); -// // Statistics allStats = dataLoaderRegistry.getStatistics(); -// // Map statsMap = new LinkedHashMap<>(); -// // statsMap.put("overall-statistics", allStats.toMap()); -// // -// // Map individualStatsMap = new LinkedHashMap<>(); -// // -// // for (String dlKey : dataLoaderRegistry.getKeys()) { -// // DataLoader dl = dataLoaderRegistry.getDataLoader(dlKey); -// // Statistics statistics = dl.getStatistics(); -// // individualStatsMap.put(dlKey, statistics.toMap()); -// // } -// // -// // statsMap.put("individual-statistics", individualStatsMap); -// // -// // return statsMap; -// // } -// // } -// ======= -// package graphql.execution.instrumentation.dataloader; -// -// import graphql.ExecutionResult; -// import graphql.ExecutionResultImpl; -// import graphql.PublicApi; -// import graphql.collect.ImmutableKit; -// import graphql.execution.AsyncExecutionStrategy; -// import graphql.execution.ExecutionContext; -// import graphql.execution.ExecutionStrategy; -// import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -// import graphql.execution.instrumentation.InstrumentationContext; -// import graphql.execution.instrumentation.InstrumentationState; -// import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; -// import graphql.execution.instrumentation.SimplePerformantInstrumentation; -// import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; -// import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; -// import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; -// import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -// import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -// import graphql.language.OperationDefinition; -// import graphql.schema.DataFetcher; -// import org.dataloader.DataLoader; -// import org.dataloader.DataLoaderRegistry; -// import org.dataloader.stats.Statistics; -// import org.jetbrains.annotations.NotNull; -// import org.jetbrains.annotations.Nullable; -// -// import java.util.LinkedHashMap; -// import java.util.Map; -// import java.util.concurrent.CompletableFuture; -// -// import static graphql.execution.instrumentation.InstrumentationState.ofState; -// import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; -// -// /** -// * This graphql {@link graphql.execution.instrumentation.Instrumentation} will dispatch -// * all the contained {@link org.dataloader.DataLoader}s when each level of the graphql -// * query is executed. -// *

-// * This allows you to use {@link org.dataloader.DataLoader}s in your {@link graphql.schema.DataFetcher}s -// * to optimal loading of data. -// *

-// * A DataLoaderDispatcherInstrumentation will be automatically added to the {@link graphql.GraphQL} -// * instrumentation list if one is not present. -// * -// * @see org.dataloader.DataLoader -// * @see org.dataloader.DataLoaderRegistry -// */ -// @PublicApi -// public class DataLoaderDispatcherInstrumentation extends SimplePerformantInstrumentation { -// private final DataLoaderDispatcherInstrumentationOptions options; -// -// /** -// * Creates a DataLoaderDispatcherInstrumentation with the default options -// */ -// public DataLoaderDispatcherInstrumentation() { -// this(DataLoaderDispatcherInstrumentationOptions.newOptions()); -// } -// -// /** -// * Creates a DataLoaderDispatcherInstrumentation with the specified options -// * -// * @param options the options to control the behaviour -// */ -// public DataLoaderDispatcherInstrumentation(DataLoaderDispatcherInstrumentationOptions options) { -// this.options = options; -// } -// -// -// @Override -// public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { -// return new DataLoaderDispatcherInstrumentationState(parameters.getExecutionInput().getDataLoaderRegistry()); -// } -// -// @Override -// public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { -// DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// if (state.isAggressivelyBatching()) { -// return dataFetcher; -// } -// // -// // currently only AsyncExecutionStrategy with DataLoader and hence this allows us to "dispatch" -// // on every object if it's not using aggressive batching for other execution strategies -// // which allows them to work if used. -// return (DataFetcher) environment -> { -// Object obj = dataFetcher.get(environment); -// immediatelyDispatch(state); -// return obj; -// }; -// } -// -// private void immediatelyDispatch(DataLoaderDispatcherInstrumentationState state) { -// state.getApproach().dispatch(); -// } -// -// @Override -// public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState rawState) { -// DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // -// // during #instrumentExecutionInput they could have enhanced the data loader registry -// // so we grab it now just before the query operation gets started -// // -// DataLoaderRegistry finalRegistry = parameters.getExecutionContext().getDataLoaderRegistry(); -// state.setDataLoaderRegistry(finalRegistry); -// if (!isDataLoaderCompatibleExecution(parameters.getExecutionContext())) { -// state.setAggressivelyBatching(false); -// } -// return noOp(); -// } -// -// private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContext) { -// // -// // Currently we only support aggressive batching for the AsyncExecutionStrategy. -// // This may change in the future but this is the fix for now. -// // -// OperationDefinition.Operation operation = executionContext.getOperationDefinition().getOperation(); -// ExecutionStrategy strategy = executionContext.getStrategy(operation); -// return (strategy instanceof AsyncExecutionStrategy); -// } -// -// @Override -// public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // -// // if there are no data loaders, there is nothing to do -// // -// if (state.hasNoDataLoaders()) { -// return ExecutionStrategyInstrumentationContext.NOOP; -// } -// return state.getApproach().beginExecutionStrategy(parameters, state.getState()); -// } -// -// @Override -// public @Nullable ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // -// // if there are no data loaders, there is nothing to do -// // -// if (state.hasNoDataLoaders()) { -// return ExecuteObjectInstrumentationContext.NOOP; -// } -// return state.getApproach().beginObjectResolution(parameters, state.getState()); -// } -// -// @Override -// public @Nullable InstrumentationContext beginDeferredField(InstrumentationState rawState) { -// DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // -// // if there are no data loaders, there is nothing to do -// // -// if (state.hasNoDataLoaders()) { -// return noOp(); -// } -// -// // The support for @defer in graphql-java is not yet complete. At this time, the resolution of deferred fields -// // cannot be done via Data Loaders. -// throw new UnsupportedOperationException("Data Loaders cannot be used to resolve deferred fields"); -// } -// -// @Override -// public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { -// DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// // -// // if there are no data loaders, there is nothing to do -// // -// if (state.hasNoDataLoaders()) { -// return noOp(); -// } -// return state.getApproach().beginFieldFetch(parameters, state.getState()); -// } -// -// @Override -// public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState rawState) { -// if (!options.isIncludeStatistics()) { -// return CompletableFuture.completedFuture(executionResult); -// } -// DataLoaderDispatcherInstrumentationState state = ofState(rawState); -// Map currentExt = executionResult.getExtensions(); -// Map statsMap = new LinkedHashMap<>(currentExt == null ? ImmutableKit.emptyMap() : currentExt); -// Map dataLoaderStats = buildStatsMap(state); -// statsMap.put("dataloader", dataLoaderStats); -// -// return CompletableFuture.completedFuture(new ExecutionResultImpl(executionResult.getData(), executionResult.getErrors(), statsMap)); -// } -// -// private Map buildStatsMap(DataLoaderDispatcherInstrumentationState state) { -// DataLoaderRegistry dataLoaderRegistry = state.getDataLoaderRegistry(); -// Statistics allStats = dataLoaderRegistry.getStatistics(); -// Map statsMap = new LinkedHashMap<>(); -// statsMap.put("overall-statistics", allStats.toMap()); -// -// Map individualStatsMap = new LinkedHashMap<>(); -// -// for (String dlKey : dataLoaderRegistry.getKeys()) { -// DataLoader dl = dataLoaderRegistry.getDataLoader(dlKey); -// Statistics statistics = dl.getStatistics(); -// individualStatsMap.put(dlKey, statistics.toMap()); -// } -// -// statsMap.put("individual-statistics", individualStatsMap); -// -// return statsMap; -// } -// } -// >>>>>>> master diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java deleted file mode 100644 index 11c2cf1553..0000000000 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationOptions.java +++ /dev/null @@ -1,38 +0,0 @@ -// package graphql.execution.instrumentation.dataloader; -// -// import graphql.PublicApi; -// -// /** -// * The options that control the operation of {@link graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation} -// */ -// @PublicApi -// public class DataLoaderDispatcherInstrumentationOptions { -// -// private final boolean includeStatistics; -// -// private DataLoaderDispatcherInstrumentationOptions(boolean includeStatistics) { -// this.includeStatistics = includeStatistics; -// } -// -// public static DataLoaderDispatcherInstrumentationOptions newOptions() { -// return new DataLoaderDispatcherInstrumentationOptions(false); -// } -// -// /** -// * This will toggle the ability to include java-dataloader statistics into the extensions -// * output of your query -// * -// * @param flag the switch to follow -// * -// * @return a new options object -// */ -// public DataLoaderDispatcherInstrumentationOptions includeStatistics(boolean flag) { -// return new DataLoaderDispatcherInstrumentationOptions(flag); -// } -// -// -// public boolean isIncludeStatistics() { -// return includeStatistics; -// } -// -// } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java deleted file mode 100644 index 9336f2c4ad..0000000000 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationState.java +++ /dev/null @@ -1,90 +0,0 @@ -// package graphql.execution.instrumentation.dataloader; -// -// import graphql.Assert; -// import graphql.Internal; -// import graphql.PublicApi; -// import graphql.execution.instrumentation.InstrumentationState; -// import org.dataloader.DataLoader; -// import org.dataloader.DataLoaderRegistry; -// -// import java.util.concurrent.atomic.AtomicReference; -// import java.util.function.Function; -// -// /** -// * A base class that keeps track of whether aggressive batching can be used -// */ -// @PublicApi -// public class DataLoaderDispatcherInstrumentationState implements InstrumentationState { -// -// @Internal -// public static final DataLoaderRegistry EMPTY_DATALOADER_REGISTRY = new DataLoaderRegistry() { -// -// private static final String ERROR_MESSAGE = "You MUST set in your own DataLoaderRegistry to use data loader"; -// -// @Override -// public DataLoaderRegistry register(String key, DataLoader dataLoader) { -// return Assert.assertShouldNeverHappen(ERROR_MESSAGE); -// } -// -// @Override -// public DataLoader computeIfAbsent(final String key, -// final Function> mappingFunction) { -// return Assert.assertShouldNeverHappen(ERROR_MESSAGE); -// } -// -// @Override -// public DataLoaderRegistry unregister(String key) { -// return Assert.assertShouldNeverHappen(ERROR_MESSAGE); -// } -// }; -// -// private final FieldLevelTrackingApproach approach; -// private final AtomicReference dataLoaderRegistry; -// private final InstrumentationState state; -// private volatile boolean aggressivelyBatching = true; -// private volatile boolean hasNoDataLoaders; -// -// public DataLoaderDispatcherInstrumentationState(DataLoaderRegistry dataLoaderRegistry) { -// this.dataLoaderRegistry = new AtomicReference<>(dataLoaderRegistry); -// this.approach = new FieldLevelTrackingApproach(this::getDataLoaderRegistry); -// this.state = approach.createState(); -// hasNoDataLoaders = checkForNoDataLoader(dataLoaderRegistry); -// } -// -// private boolean checkForNoDataLoader(DataLoaderRegistry dataLoaderRegistry) { -// // -// // if they have never set a dataloader into the execution input then we can optimize -// // away the tracking code -// // -// return dataLoaderRegistry == EMPTY_DATALOADER_REGISTRY; -// } -// -// boolean isAggressivelyBatching() { -// return aggressivelyBatching; -// } -// -// void setAggressivelyBatching(boolean aggressivelyBatching) { -// this.aggressivelyBatching = aggressivelyBatching; -// } -// -// FieldLevelTrackingApproach getApproach() { -// return approach; -// } -// -// DataLoaderRegistry getDataLoaderRegistry() { -// return dataLoaderRegistry.get(); -// } -// -// void setDataLoaderRegistry(DataLoaderRegistry newRegistry) { -// dataLoaderRegistry.set(newRegistry); -// hasNoDataLoaders = checkForNoDataLoader(newRegistry); -// } -// -// boolean hasNoDataLoaders() { -// return hasNoDataLoaders; -// } -// -// InstrumentationState getState() { -// return state; -// } -// } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java deleted file mode 100644 index 5ee434d8f5..0000000000 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ /dev/null @@ -1,572 +0,0 @@ -// <<<<<<< HEAD -// // package graphql.execution.instrumentation.dataloader; -// // -// // import graphql.Assert; -// // import graphql.ExecutionResult; -// // import graphql.Internal; -// // import graphql.execution.FieldValueInfo; -// // import graphql.execution.ResultPath; -// // import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -// // import graphql.execution.instrumentation.InstrumentationContext; -// // import graphql.execution.instrumentation.InstrumentationState; -// // import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; -// // import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -// // import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -// // import org.dataloader.DataLoaderRegistry; -// // -// // import java.util.LinkedHashSet; -// // import java.util.List; -// // import java.util.Map; -// // import java.util.Set; -// // import java.util.concurrent.CompletableFuture; -// // import java.util.function.Supplier; -// // -// // /** -// // * This approach uses field level tracking to achieve its aims of making the data loader more efficient -// // */ -// // @Internal -// // public class FieldLevelTrackingApproach { -// // private final Supplier dataLoaderRegistrySupplier; -// // private static class CallStack implements InstrumentationState { -// // -// // private final LevelMap expectedFetchCountPerLevel = new LevelMap(); -// // private final LevelMap fetchCountPerLevel = new LevelMap(); -// // private final LevelMap expectedStrategyCallsPerLevel = new LevelMap(); -// // private final LevelMap happenedStrategyCallsPerLevel = new LevelMap(); -// // private final LevelMap happenedOnFieldValueCallsPerLevel = new LevelMap(); -// // -// // private final Set dispatchedLevels = new LinkedHashSet<>(); -// // -// // CallStack() { -// // expectedStrategyCallsPerLevel.set(1, 1); -// // } -// // -// // void increaseExpectedFetchCount(int level, int count) { -// // expectedFetchCountPerLevel.increment(level, count); -// // } -// // -// // void increaseFetchCount(int level) { -// // fetchCountPerLevel.increment(level, 1); -// // } -// // -// // void increaseExpectedStrategyCalls(int level, int count) { -// // expectedStrategyCallsPerLevel.increment(level, count); -// // } -// // -// // void increaseHappenedStrategyCalls(int level) { -// // happenedStrategyCallsPerLevel.increment(level, 1); -// // } -// // -// // void increaseHappenedOnFieldValueCalls(int level) { -// // happenedOnFieldValueCallsPerLevel.increment(level, 1); -// // } -// // -// // boolean allStrategyCallsHappened(int level) { -// // return happenedStrategyCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); -// // } -// // -// // boolean allOnFieldCallsHappened(int level) { -// // return happenedOnFieldValueCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); -// // } -// // -// // boolean allFetchesHappened(int level) { -// // return fetchCountPerLevel.get(level) == expectedFetchCountPerLevel.get(level); -// // } -// // -// // @Override -// // public String toString() { -// // return "CallStack{" + -// // "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + -// // ", fetchCountPerLevel=" + fetchCountPerLevel + -// // ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + -// // ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + -// // ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + -// // ", dispatchedLevels" + dispatchedLevels + -// // '}'; -// // } -// // -// // public boolean dispatchIfNotDispatchedBefore(int level) { -// // if (dispatchedLevels.contains(level)) { -// // Assert.assertShouldNeverHappen("level " + level + " already dispatched"); -// // return false; -// // } -// // dispatchedLevels.add(level); -// // return true; -// // } -// // -// // public void clearAndMarkCurrentLevelAsReady(int level) { -// // expectedFetchCountPerLevel.clear(); -// // fetchCountPerLevel.clear(); -// // expectedStrategyCallsPerLevel.clear(); -// // happenedStrategyCallsPerLevel.clear(); -// // happenedOnFieldValueCallsPerLevel.clear(); -// // dispatchedLevels.clear(); -// // -// // // make sure the level is ready -// // expectedFetchCountPerLevel.increment(level, 1); -// // expectedStrategyCallsPerLevel.increment(level, 1); -// // happenedStrategyCallsPerLevel.increment(level, 1); -// // } -// // } -// // -// // public FieldLevelTrackingApproach(Supplier dataLoaderRegistrySupplier) { -// // this.dataLoaderRegistrySupplier = dataLoaderRegistrySupplier; -// // } -// // -// // public InstrumentationState createState() { -// // return new CallStack(); -// // } -// // -// // ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// // CallStack callStack = (CallStack) rawState; -// // int curLevel = getCurrentLevel(parameters); -// // increaseCallCounts(callStack, curLevel, parameters); -// // -// // return new ExecutionStrategyInstrumentationContext() { -// // @Override -// // public void onDispatched(CompletableFuture result) { -// // -// // } -// // -// // @Override -// // public void onCompleted(ExecutionResult result, Throwable t) { -// // -// // } -// // -// // @Override -// // public void onFieldValuesInfo(List fieldValueInfoList) { -// // onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); -// // } -// // -// // @Override -// // public void onFieldValuesException() { -// // synchronized (callStack) { -// // callStack.increaseHappenedOnFieldValueCalls(curLevel); -// // } -// // } -// // }; -// // } -// // -// // ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// // CallStack callStack = (CallStack) rawState; -// // int curLevel = getCurrentLevel(parameters); -// // increaseCallCounts(callStack, curLevel, parameters); -// // -// // return new ExecuteObjectInstrumentationContext() { -// // -// // @Override -// // public void onDispatched(CompletableFuture> result) { -// // } -// // -// // @Override -// // public void onCompleted(Map result, Throwable t) { -// // } -// // -// // @Override -// // public void onFieldValuesInfo(List fieldValueInfoList) { -// // onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); -// // } -// // -// // @Override -// // public void onFieldValuesException() { -// // synchronized (callStack) { -// // callStack.increaseHappenedOnFieldValueCalls(curLevel); -// // } -// // } -// // }; -// // } -// // -// // private int getCurrentLevel(InstrumentationExecutionStrategyParameters parameters) { -// // ResultPath path = parameters.getExecutionStrategyParameters().getPath(); -// // return path.getLevel() + 1; -// // } -// // -// // @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") -// // private static void increaseCallCounts(CallStack callStack, int curLevel, InstrumentationExecutionStrategyParameters parameters) { -// // int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); -// // synchronized (callStack) { -// // callStack.increaseExpectedFetchCount(curLevel, fieldCount); -// // callStack.increaseHappenedStrategyCalls(curLevel); -// // } -// // } -// // -// // @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") -// // private void onFieldValuesInfoDispatchIfNeeded(CallStack callStack, List fieldValueInfoList, int curLevel) { -// // boolean dispatchNeeded; -// // synchronized (callStack) { -// // dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); -// // } -// // if (dispatchNeeded) { -// // dispatch(); -// // } -// // } -// // -// // // -// // // thread safety: called with synchronised(callStack) -// // // -// // private boolean handleOnFieldValuesInfo(List fieldValueInfos, CallStack callStack, int curLevel) { -// // callStack.increaseHappenedOnFieldValueCalls(curLevel); -// // int expectedStrategyCalls = getCountForList(fieldValueInfos); -// // callStack.increaseExpectedStrategyCalls(curLevel + 1, expectedStrategyCalls); -// // return dispatchIfNeeded(callStack, curLevel + 1); -// // } -// // -// // private int getCountForList(List fieldValueInfos) { -// // int result = 0; -// // for (FieldValueInfo fieldValueInfo : fieldValueInfos) { -// // if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { -// // result += 1; -// // } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { -// // result += getCountForList(fieldValueInfo.getFieldValueInfos()); -// // } -// // } -// // return result; -// // } -// // -// // -// // public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { -// // CallStack callStack = (CallStack) rawState; -// // ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); -// // int level = path.getLevel(); -// // return new InstrumentationContext() { -// // -// // @Override -// // public void onDispatched(CompletableFuture result) { -// // boolean dispatchNeeded; -// // synchronized (callStack) { -// // callStack.increaseFetchCount(level); -// // dispatchNeeded = dispatchIfNeeded(callStack, level); -// // } -// // if (dispatchNeeded) { -// // dispatch(); -// // } -// // -// // } -// // -// // @Override -// // public void onCompleted(Object result, Throwable t) { -// // } -// // }; -// // } -// // -// // -// // // -// // // thread safety : called with synchronised(callStack) -// // // -// // private boolean dispatchIfNeeded(CallStack callStack, int level) { -// // if (levelReady(callStack, level)) { -// // return callStack.dispatchIfNotDispatchedBefore(level); -// // } -// // return false; -// // } -// // -// // // -// // // thread safety: called with synchronised(callStack) -// // // -// // private boolean levelReady(CallStack callStack, int level) { -// // if (level == 1) { -// // // level 1 is special: there is only one strategy call and that's it -// // return callStack.allFetchesHappened(1); -// // } -// // if (levelReady(callStack, level - 1) && callStack.allOnFieldCallsHappened(level - 1) -// // && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { -// // return true; -// // } -// // return false; -// // } -// // -// // void dispatch() { -// // DataLoaderRegistry dataLoaderRegistry = getDataLoaderRegistry(); -// // dataLoaderRegistry.dispatchAll(); -// // } -// // -// // private DataLoaderRegistry getDataLoaderRegistry() { -// // return dataLoaderRegistrySupplier.get(); -// // } -// // } -// ======= -// package graphql.execution.instrumentation.dataloader; -// -// import graphql.Assert; -// import graphql.ExecutionResult; -// import graphql.Internal; -// import graphql.execution.FieldValueInfo; -// import graphql.execution.ResultPath; -// import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -// import graphql.execution.instrumentation.InstrumentationContext; -// import graphql.execution.instrumentation.InstrumentationState; -// import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; -// import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -// import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -// import org.dataloader.DataLoaderRegistry; -// -// import java.util.LinkedHashSet; -// import java.util.List; -// import java.util.Map; -// import java.util.Set; -// import java.util.concurrent.CompletableFuture; -// import java.util.function.Supplier; -// -// /** -// * This approach uses field level tracking to achieve its aims of making the data loader more efficient -// */ -// @Internal -// public class FieldLevelTrackingApproach { -// private final Supplier dataLoaderRegistrySupplier; -// private static class CallStack implements InstrumentationState { -// -// private final LevelMap expectedFetchCountPerLevel = new LevelMap(); -// private final LevelMap fetchCountPerLevel = new LevelMap(); -// private final LevelMap expectedStrategyCallsPerLevel = new LevelMap(); -// private final LevelMap happenedStrategyCallsPerLevel = new LevelMap(); -// private final LevelMap happenedOnFieldValueCallsPerLevel = new LevelMap(); -// -// private final Set dispatchedLevels = new LinkedHashSet<>(); -// -// CallStack() { -// expectedStrategyCallsPerLevel.set(1, 1); -// } -// -// void increaseExpectedFetchCount(int level, int count) { -// expectedFetchCountPerLevel.increment(level, count); -// } -// -// void increaseFetchCount(int level) { -// fetchCountPerLevel.increment(level, 1); -// } -// -// void increaseExpectedStrategyCalls(int level, int count) { -// expectedStrategyCallsPerLevel.increment(level, count); -// } -// -// void increaseHappenedStrategyCalls(int level) { -// happenedStrategyCallsPerLevel.increment(level, 1); -// } -// -// void increaseHappenedOnFieldValueCalls(int level) { -// happenedOnFieldValueCallsPerLevel.increment(level, 1); -// } -// -// boolean allStrategyCallsHappened(int level) { -// return happenedStrategyCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); -// } -// -// boolean allOnFieldCallsHappened(int level) { -// return happenedOnFieldValueCallsPerLevel.get(level) == expectedStrategyCallsPerLevel.get(level); -// } -// -// boolean allFetchesHappened(int level) { -// return fetchCountPerLevel.get(level) == expectedFetchCountPerLevel.get(level); -// } -// -// @Override -// public String toString() { -// return "CallStack{" + -// "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + -// ", fetchCountPerLevel=" + fetchCountPerLevel + -// ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + -// ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + -// ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + -// ", dispatchedLevels" + dispatchedLevels + -// '}'; -// } -// -// public boolean dispatchIfNotDispatchedBefore(int level) { -// if (dispatchedLevels.contains(level)) { -// Assert.assertShouldNeverHappen("level " + level + " already dispatched"); -// return false; -// } -// dispatchedLevels.add(level); -// return true; -// } -// -// public void clearAndMarkCurrentLevelAsReady(int level) { -// expectedFetchCountPerLevel.clear(); -// fetchCountPerLevel.clear(); -// expectedStrategyCallsPerLevel.clear(); -// happenedStrategyCallsPerLevel.clear(); -// happenedOnFieldValueCallsPerLevel.clear(); -// dispatchedLevels.clear(); -// -// // make sure the level is ready -// expectedFetchCountPerLevel.increment(level, 1); -// expectedStrategyCallsPerLevel.increment(level, 1); -// happenedStrategyCallsPerLevel.increment(level, 1); -// } -// } -// -// public FieldLevelTrackingApproach(Supplier dataLoaderRegistrySupplier) { -// this.dataLoaderRegistrySupplier = dataLoaderRegistrySupplier; -// } -// -// public InstrumentationState createState() { -// return new CallStack(); -// } -// -// ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// CallStack callStack = (CallStack) rawState; -// int curLevel = getCurrentLevel(parameters); -// increaseCallCounts(callStack, curLevel, parameters); -// -// return new ExecutionStrategyInstrumentationContext() { -// @Override -// public void onDispatched(CompletableFuture result) { -// -// } -// -// @Override -// public void onCompleted(ExecutionResult result, Throwable t) { -// -// } -// -// @Override -// public void onFieldValuesInfo(List fieldValueInfoList) { -// onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); -// } -// -// @Override -// public void onFieldValuesException() { -// synchronized (callStack) { -// callStack.increaseHappenedOnFieldValueCalls(curLevel); -// } -// } -// }; -// } -// -// ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { -// CallStack callStack = (CallStack) rawState; -// int curLevel = getCurrentLevel(parameters); -// increaseCallCounts(callStack, curLevel, parameters); -// -// return new ExecuteObjectInstrumentationContext() { -// -// @Override -// public void onDispatched(CompletableFuture> result) { -// } -// -// @Override -// public void onCompleted(Map result, Throwable t) { -// } -// -// @Override -// public void onFieldValuesInfo(List fieldValueInfoList) { -// onFieldValuesInfoDispatchIfNeeded(callStack, fieldValueInfoList, curLevel); -// } -// -// @Override -// public void onFieldValuesException() { -// synchronized (callStack) { -// callStack.increaseHappenedOnFieldValueCalls(curLevel); -// } -// } -// }; -// } -// -// private int getCurrentLevel(InstrumentationExecutionStrategyParameters parameters) { -// ResultPath path = parameters.getExecutionStrategyParameters().getPath(); -// return path.getLevel() + 1; -// } -// -// @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") -// private static void increaseCallCounts(CallStack callStack, int curLevel, InstrumentationExecutionStrategyParameters parameters) { -// int fieldCount = parameters.getExecutionStrategyParameters().getFields().size(); -// synchronized (callStack) { -// callStack.increaseExpectedFetchCount(curLevel, fieldCount); -// callStack.increaseHappenedStrategyCalls(curLevel); -// } -// } -// -// @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") -// private void onFieldValuesInfoDispatchIfNeeded(CallStack callStack, List fieldValueInfoList, int curLevel) { -// boolean dispatchNeeded; -// synchronized (callStack) { -// dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, callStack, curLevel); -// } -// if (dispatchNeeded) { -// dispatch(); -// } -// } -// -// // -// // thread safety: called with synchronised(callStack) -// // -// private boolean handleOnFieldValuesInfo(List fieldValueInfos, CallStack callStack, int curLevel) { -// callStack.increaseHappenedOnFieldValueCalls(curLevel); -// int expectedStrategyCalls = getCountForList(fieldValueInfos); -// callStack.increaseExpectedStrategyCalls(curLevel + 1, expectedStrategyCalls); -// return dispatchIfNeeded(callStack, curLevel + 1); -// } -// -// private int getCountForList(List fieldValueInfos) { -// int result = 0; -// for (FieldValueInfo fieldValueInfo : fieldValueInfos) { -// if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.OBJECT) { -// result += 1; -// } else if (fieldValueInfo.getCompleteValueType() == FieldValueInfo.CompleteValueType.LIST) { -// result += getCountForList(fieldValueInfo.getFieldValueInfos()); -// } -// } -// return result; -// } -// -// public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { -// CallStack callStack = (CallStack) rawState; -// ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); -// int level = path.getLevel(); -// return new InstrumentationContext() { -// -// @Override -// public void onDispatched(CompletableFuture result) { -// boolean dispatchNeeded; -// synchronized (callStack) { -// callStack.increaseFetchCount(level); -// dispatchNeeded = dispatchIfNeeded(callStack, level); -// } -// if (dispatchNeeded) { -// dispatch(); -// } -// -// } -// -// @Override -// public void onCompleted(Object result, Throwable t) { -// } -// }; -// } -// -// -// // -// // thread safety : called with synchronised(callStack) -// // -// private boolean dispatchIfNeeded(CallStack callStack, int level) { -// if (levelReady(callStack, level)) { -// return callStack.dispatchIfNotDispatchedBefore(level); -// } -// return false; -// } -// -// // -// // thread safety: called with synchronised(callStack) -// // -// private boolean levelReady(CallStack callStack, int level) { -// if (level == 1) { -// // level 1 is special: there is only one strategy call and that's it -// return callStack.allFetchesHappened(1); -// } -// if (levelReady(callStack, level - 1) && callStack.allOnFieldCallsHappened(level - 1) -// && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { -// return true; -// } -// return false; -// } -// -// void dispatch() { -// DataLoaderRegistry dataLoaderRegistry = getDataLoaderRegistry(); -// dataLoaderRegistry.dispatchAll(); -// } -// -// private DataLoaderRegistry getDataLoaderRegistry() { -// return dataLoaderRegistrySupplier.get(); -// } -// } -// >>>>>>> master From 64e686fdf4f592893eede3dfadb5fc75462d2fdd Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 15 Feb 2024 09:25:47 +1100 Subject: [PATCH 184/393] Rename DeferExecution to NormalizedDeferredExecution This will align the name of this class, which is in the 'incremental' package, with 'DeferredExecution', which is in the 'execution' package. --- .../incremental/DeferredExecution.java | 4 +- .../java/graphql/normalized/ENFMerger.java | 2 +- .../normalized/ExecutableNormalizedField.java | 32 +++++----- .../ExecutableNormalizedOperationFactory.java | 62 +++++++++---------- ...tableNormalizedOperationToAstCompiler.java | 32 +++++----- .../incremental/IncrementalNodes.java | 6 +- ....java => NormalizedDeferredExecution.java} | 22 +++---- ...NormalizedOperationFactoryDeferTest.groovy | 40 ++++++------ 8 files changed, 100 insertions(+), 100 deletions(-) rename src/main/java/graphql/normalized/incremental/{DeferExecution.java => NormalizedDeferredExecution.java} (73%) diff --git a/src/main/java/graphql/execution/incremental/DeferredExecution.java b/src/main/java/graphql/execution/incremental/DeferredExecution.java index b8560b8535..3f14f5922e 100644 --- a/src/main/java/graphql/execution/incremental/DeferredExecution.java +++ b/src/main/java/graphql/execution/incremental/DeferredExecution.java @@ -1,14 +1,14 @@ package graphql.execution.incremental; import graphql.ExperimentalApi; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.NormalizedDeferredExecution; import javax.annotation.Nullable; /** * Represents details about the defer execution that can be associated with a {@link graphql.execution.MergedField}. *

- * This representation is used during graphql execution. Check {@link DeferExecution} + * This representation is used during graphql execution. Check {@link NormalizedDeferredExecution} * for the normalized representation of @defer. */ @ExperimentalApi diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index f1fbc37b95..5150eee5a4 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -75,7 +75,7 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) if (deferSupport) { // Move defer executions from removed field into the merged field's entry - first.addDeferExecutions(next.getDeferExecutions()); + first.addDeferredExecutions(next.getDeferredExecutions()); } } first.setObjectTypeNames(mergedObjects); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 3a8d36af08..47f8151229 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -10,7 +10,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.NormalizedDeferredExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -66,7 +66,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferredExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -78,7 +78,7 @@ private ExecutableNormalizedField(Builder builder) { this.children = builder.children; this.level = builder.level; this.parent = builder.parent; - this.deferExecutions = builder.deferExecutions; + this.deferredExecutions = builder.deferredExecutions; } /** @@ -262,13 +262,13 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { - this.deferExecutions.clear(); - this.deferExecutions.addAll(deferExecutions); + public void setDeferredExecutions(Collection deferredExecutions) { + this.deferredExecutions.clear(); + this.deferredExecutions.addAll(deferredExecutions); } - public void addDeferExecutions(Collection deferExecutions) { - this.deferExecutions.addAll(deferExecutions); + public void addDeferredExecutions(Collection deferredExecutions) { + this.deferredExecutions.addAll(deferredExecutions); } /** @@ -477,12 +477,12 @@ public ExecutableNormalizedField getParent() { } /** - * @return the {@link DeferExecution}s associated with this {@link ExecutableNormalizedField}. - * @see DeferExecution + * @return the {@link NormalizedDeferredExecution}s associated with this {@link ExecutableNormalizedField}. + * @see NormalizedDeferredExecution */ @ExperimentalApi - public LinkedHashSet getDeferExecutions() { - return deferExecutions; + public LinkedHashSet getDeferredExecutions() { + return deferredExecutions; } @Internal @@ -612,7 +612,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferredExecutions = new LinkedHashSet<>(); private Builder() { } @@ -627,7 +627,7 @@ private Builder(ExecutableNormalizedField existing) { this.children = new ArrayList<>(existing.children); this.level = existing.getLevel(); this.parent = existing.getParent(); - this.deferExecutions = existing.getDeferExecutions(); + this.deferredExecutions = existing.getDeferredExecutions(); } public Builder clearObjectTypesNames() { @@ -683,8 +683,8 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { - this.deferExecutions = deferExecutions; + public Builder deferredExecutions(LinkedHashSet deferredExecutions) { + this.deferredExecutions = deferredExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 9367c1d58d..e739db90f2 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -30,7 +30,7 @@ import graphql.language.SelectionSet; import graphql.language.TypeName; import graphql.language.VariableDefinition; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.NormalizedDeferredExecution; import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; @@ -568,7 +568,7 @@ private void createNFs(ImmutableList.Builder nfListBu nfListBuilder.add(nf); if (this.options.deferSupport) { - nf.addDeferExecutions(fieldGroup.deferExecutions); + nf.addDeferredExecutions(fieldGroup.deferredExecutions); } } if (commonParentsGroups.size() > 1) { @@ -633,22 +633,22 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder deferredExecutionsBuilder = ImmutableSet.builder(); for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferExecution collectedDeferExecution = collectedField.deferExecution; + NormalizedDeferredExecution collectedDeferredExecution = collectedField.deferredExecution; - if (collectedDeferExecution != null) { - deferExecutionsBuilder.add(collectedDeferExecution); + if (collectedDeferredExecution != null) { + deferredExecutionsBuilder.add(collectedDeferredExecution); } } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build(); + Set deferredExecutions = deferredExecutionsBuilder.build(); - Set duplicatedLabels = listDuplicatedLabels(deferExecutions); + Set duplicatedLabels = listDuplicatedLabels(deferredExecutions); if (!duplicatedLabels.isEmpty()) { // Query validation should pick this up @@ -657,33 +657,33 @@ private List groupByCommonParentsWithDeferSupport(Collectio Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecutions)); + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferredExecutions)); } ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - Set filteredDeferExecutions = deferExecutions.stream() + Set filteredDeferredExecutions = deferredExecutions.stream() .filter(filterExecutionsFromType(objectType)) .collect(toCollection(LinkedHashSet::new)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferredExecutions)); } return result.build(); } - private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { + private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { String objectTypeName = objectType.getName(); - return deferExecution -> deferExecution.getPossibleTypes() + return deferredExecution -> deferredExecution.getPossibleTypes() .stream() .map(GraphQLObjectType::getName) .anyMatch(objectTypeName::equals); } - private Set listDuplicatedLabels(Collection deferExecutions) { - return deferExecutions.stream() - .map(DeferExecution::getLabel) + private Set listDuplicatedLabels(Collection deferredExecutions) { + return deferredExecutions.stream() + .map(NormalizedDeferredExecution::getLabel) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() @@ -697,11 +697,11 @@ private void collectFromSelectionSet(SelectionSet selectionSet, List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferExecution deferExecution + NormalizedDeferredExecution deferredExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { - collectField(result, (Field) selection, possibleObjects, astTypeCondition, deferExecution); + collectField(result, (Field) selection, possibleObjects, astTypeCondition, deferredExecution); } else if (selection instanceof InlineFragment) { collectInlineFragment(result, (InlineFragment) selection, possibleObjects, astTypeCondition); } else if (selection instanceof FragmentSpread) { @@ -731,12 +731,12 @@ private void collectFragmentSpread(List result, GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(this.graphQLSchema.getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition); - DeferExecution newDeferExecution = buildDeferExecution( + NormalizedDeferredExecution newDeferredExecution = buildDeferredExecution( fragmentSpread.getDirectives(), fragmentDefinition.getTypeCondition(), newPossibleObjects); - collectFromSelectionSet(fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); + collectFromSelectionSet(fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferredExecution); } private void collectInlineFragment(List result, @@ -756,16 +756,16 @@ private void collectInlineFragment(List result, } - DeferExecution newDeferExecution = buildDeferExecution( + NormalizedDeferredExecution newDeferredExecution = buildDeferredExecution( inlineFragment.getDirectives(), inlineFragment.getTypeCondition(), newPossibleObjects ); - collectFromSelectionSet(inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); + collectFromSelectionSet(inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferredExecution); } - private DeferExecution buildDeferExecution( + private NormalizedDeferredExecution buildDeferredExecution( List directives, TypeName typeCondition, Set newPossibleObjects) { @@ -785,7 +785,7 @@ private void collectField(List result, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferExecution deferExecution + NormalizedDeferredExecution deferredExecution ) { if (!conditionalNodes.shouldInclude(field, this.coercedVariableValues.toMap(), @@ -797,7 +797,7 @@ private void collectField(List result, if (possibleObjectTypes.isEmpty()) { return; } - result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferExecution)); + result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferredExecution)); } private Set narrowDownPossibleObjects(Set currentOnes, @@ -852,13 +852,13 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; - DeferExecution deferExecution; + NormalizedDeferredExecution deferredExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, NormalizedDeferredExecution deferredExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; - this.deferExecution = deferExecution; + this.deferredExecution = deferredExecution; } } @@ -885,12 +885,12 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { private static class CollectedFieldGroup { Set objectTypes; Set fields; - Set deferExecutions; + Set deferredExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferredExecutions) { this.fields = fields; this.objectTypes = objectTypes; - this.deferExecutions = deferExecutions; + this.deferredExecutions = deferredExecutions; } } } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 051d4655de..877decb767 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,7 +23,7 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.NormalizedDeferredExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -277,30 +277,30 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferredExecutions = nf.getDeferredExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) .forEach((objectTypeName, field) -> { - if (deferExecutions == null || deferExecutions.isEmpty()) { + if (deferredExecutions == null || deferredExecutions.isEmpty()) { fieldsByFragmentDetails .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) .add(field); } else { - deferExecutions.forEach(deferExecution -> { + deferredExecutions.forEach(deferredExecution -> { fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferredExecution), ignored -> new ArrayList<>()) .add(field); }); } }); - } else if (deferExecutions != null && !deferExecutions.isEmpty()) { + } else if (deferredExecutions != null && !deferredExecutions.isEmpty()) { Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true); - deferExecutions.forEach(deferExecution -> { + deferredExecutions.forEach(deferredExecution -> { fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(null, deferExecution), ignored -> new ArrayList<>()) + .computeIfAbsent(new ExecutionFragmentDetails(null, deferredExecution), ignored -> new ArrayList<>()) .add(field); }); } else { @@ -317,11 +317,11 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor fragmentBuilder.typeCondition(typeName); } - if (typeAndDeferPair.deferExecution != null) { + if (typeAndDeferPair.deferredExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); + if (typeAndDeferPair.deferredExecution.getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferredExecution.getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -489,11 +489,11 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferExecution deferExecution; + private final NormalizedDeferredExecution deferredExecution; - public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, NormalizedDeferredExecution deferredExecution) { this.typeName = typeName; - this.deferExecution = deferExecution; + this.deferredExecution = deferredExecution; } @Override @@ -505,12 +505,12 @@ public boolean equals(Object o) { return false; } ExecutionFragmentDetails that = (ExecutionFragmentDetails) o; - return Objects.equals(typeName, that.typeName) && Objects.equals(deferExecution, that.deferExecution); + return Objects.equals(typeName, that.typeName) && Objects.equals(deferredExecution, that.deferredExecution); } @Override public int hashCode() { - return Objects.hash(typeName, deferExecution); + return Objects.hash(typeName, deferredExecution); } } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 025b9333a0..0daa539329 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -21,7 +21,7 @@ @Internal public class IncrementalNodes { - public DeferExecution createDeferExecution( + public NormalizedDeferredExecution createDeferExecution( Map variables, List directives, @Nullable TypeName targetType, @@ -42,12 +42,12 @@ public DeferExecution createDeferExecution( Object label = argumentValues.get("label"); if (label == null) { - return new DeferExecution(null, possibleTypes); + return new NormalizedDeferredExecution(null, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label, possibleTypes); + return new NormalizedDeferredExecution((String) label, possibleTypes); } return null; diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferredExecution.java similarity index 73% rename from src/main/java/graphql/normalized/incremental/DeferExecution.java rename to src/main/java/graphql/normalized/incremental/NormalizedDeferredExecution.java index 71488b03ca..a3f789e063 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferredExecution.java @@ -17,7 +17,7 @@ * type Dog implements Animal { name: String, age: Int } * * - * An ENF can be associated with multiple `DeferExecution`s + * An ENF can be associated with multiple `NormalizedDeferredExecution`s * * For example, this query: *

@@ -33,12 +33,12 @@
  *     }
  * 
* - * Would result in one ENF (name) associated with 2 `DeferExecution` instances. This is relevant for the execution + * Would result in one ENF (name) associated with 2 `NormalizedDeferredExecution` instances. This is relevant for the execution * since the field would have to be included in 2 incremental payloads. (I know, there's some duplication here, but * this is the current state of the spec. There are some discussions happening around de-duplicating data in scenarios * like this, so this behaviour might change in the future). * - * A `DeferExecution` may be associated with a list of possible types + * A `NormalizedDeferredExecution` may be associated with a list of possible types * * For example, this query: *
@@ -50,9 +50,9 @@
  *         }
  *     }
  * 
- * results in a `DeferExecution` with no label and possible types [Dog, Cat] + * results in a `NormalizedDeferredExecution` with no label and possible types [Dog, Cat] * - * A `DeferExecution` may be associated with specific types + * A `NormalizedDeferredExecution` may be associated with specific types * For example, this query: *
  *     query MyQuery {
@@ -66,10 +66,10 @@
  *         }
  *     }
  * 
- * results in a single ENF (name) associated with a `DeferExecution` with only "Cat" as a possible type. This means + * results in a single ENF (name) associated with a `NormalizedDeferredExecution` with only "Cat" as a possible type. This means * that, at execution time, `name` should be deferred only if the return object is a "Cat" (but not a if it is a "Dog"). * - * ENFs associated with the same instance of `DeferExecution` will be resolved in the same incremental response payload + * ENFs associated with the same instance of `NormalizedDeferredExecution` will be resolved in the same incremental response payload * For example, take these queries: * *
@@ -94,20 +94,20 @@
  *     }
  * 
* - * In `Query1`, the ENFs name and age are associated with different instances of `DeferExecution`. This means that, + * In `Query1`, the ENFs name and age are associated with different instances of `NormalizedDeferredExecution`. This means that, * during execution, `name` and `age` can be delivered at different times (if name is resolved faster, it will be * delivered first, and vice-versa). - * In `Query2` the fields will share the same instance of `DeferExecution`. This ensures that, at execution time, the + * In `Query2` the fields will share the same instance of `NormalizedDeferredExecution`. This ensures that, at execution time, the * fields are guaranteed to be delivered together. In other words, execution should wait until the slowest field resolves * and deliver both fields at the same time. * */ @ExperimentalApi -public class DeferExecution { +public class NormalizedDeferredExecution { private final String label; private final Set possibleTypes; - public DeferExecution(@Nullable String label, Set possibleTypes) { + public NormalizedDeferredExecution(@Nullable String label, Set possibleTypes) { this.label = label; this.possibleTypes = possibleTypes; } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 08ead8f5e2..332d1d685e 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -200,16 +200,16 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { def nameField = findField(executableNormalizedOperation, "Dog", "name") def ageField = findField(executableNormalizedOperation, "Dog", "age") - nameField.deferExecutions.size() == 1 - ageField.deferExecutions.size() == 2 + nameField.deferredExecutions.size() == 1 + ageField.deferredExecutions.size() == 2 // age field is associated with 2 defer executions, one of then is shared with "name" the other isn't - ageField.deferExecutions.any { - it == nameField.deferExecutions[0] + ageField.deferredExecutions.any { + it == nameField.deferredExecutions[0] } - ageField.deferExecutions.any { - it != nameField.deferExecutions[0] + ageField.deferredExecutions.any { + it != nameField.deferredExecutions[0] } printedTree == ['Query.dog', @@ -440,21 +440,21 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") - nameField.deferExecutions.size() == 3 - dogBreedField.deferExecutions.size() == 1 - catBreedField.deferExecutions.size() == 1 + nameField.deferredExecutions.size() == 3 + dogBreedField.deferredExecutions.size() == 1 + catBreedField.deferredExecutions.size() == 1 // nameField should share a defer block with each of the other fields - nameField.deferExecutions.any { - it == dogBreedField.deferExecutions[0] + nameField.deferredExecutions.any { + it == dogBreedField.deferredExecutions[0] } - nameField.deferExecutions.any { - it == catBreedField.deferExecutions[0] + nameField.deferredExecutions.any { + it == catBreedField.deferredExecutions[0] } // also, nameField should have a defer block that is not shared with any other field - nameField.deferExecutions.any { - it != dogBreedField.deferExecutions[0] && - it != catBreedField.deferExecutions[0] + nameField.deferredExecutions.any { + it != dogBreedField.deferredExecutions[0] && + it != catBreedField.deferredExecutions[0] } printedTree == ['Query.animal', @@ -491,11 +491,11 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { def nameField = findField(executableNormalizedOperation, "Dog", "name") def breedField = findField(executableNormalizedOperation, "Dog", "breed") - nameField.deferExecutions.size() == 1 - breedField.deferExecutions.size() == 1 + nameField.deferredExecutions.size() == 1 + breedField.deferredExecutions.size() == 1 // different label instances - nameField.deferExecutions[0] != breedField.deferExecutions[0] + nameField.deferredExecutions[0] != breedField.deferredExecutions[0] printedTree == ['Query.dog', 'Dog.name defer{[label=null;types=[Dog]]}', @@ -932,7 +932,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } String printDeferExecutionDetails(ExecutableNormalizedField field) { - def deferExecutions = field.deferExecutions + def deferExecutions = field.deferredExecutions if (deferExecutions == null || deferExecutions.isEmpty()) { return "" } From 86f577c332e9b21bb1dc87b5f9325d02278a7394 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 15 Feb 2024 09:42:28 +1100 Subject: [PATCH 185/393] Remove duplicated code in IncrementalNodes --- .../ExecutableNormalizedOperationFactory.java | 12 +--- .../incremental/IncrementalNodes.java | 55 ------------------- 2 files changed, 3 insertions(+), 64 deletions(-) delete mode 100644 src/main/java/graphql/normalized/incremental/IncrementalNodes.java diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index e739db90f2..96d5193919 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -17,6 +17,7 @@ import graphql.execution.conditional.ConditionalNodes; import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; +import graphql.execution.incremental.IncrementalUtils; import graphql.introspection.Introspection; import graphql.language.Directive; import graphql.language.Document; @@ -28,10 +29,8 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; -import graphql.language.TypeName; import graphql.language.VariableDefinition; import graphql.normalized.incremental.NormalizedDeferredExecution; -import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -191,7 +190,6 @@ public boolean getDeferSupport() { } private static final ConditionalNodes conditionalNodes = new ConditionalNodes(); - private static final IncrementalNodes incrementalNodes = new IncrementalNodes(); private ExecutableNormalizedOperationFactory() { @@ -733,7 +731,6 @@ private void collectFragmentSpread(List result, NormalizedDeferredExecution newDeferredExecution = buildDeferredExecution( fragmentSpread.getDirectives(), - fragmentDefinition.getTypeCondition(), newPossibleObjects); collectFromSelectionSet(fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferredExecution); @@ -758,7 +755,6 @@ private void collectInlineFragment(List result, NormalizedDeferredExecution newDeferredExecution = buildDeferredExecution( inlineFragment.getDirectives(), - inlineFragment.getTypeCondition(), newPossibleObjects ); @@ -767,17 +763,15 @@ private void collectInlineFragment(List result, private NormalizedDeferredExecution buildDeferredExecution( List directives, - TypeName typeCondition, Set newPossibleObjects) { if(!options.deferSupport) { return null; } - return incrementalNodes.createDeferExecution( + return IncrementalUtils.createDeferredExecution( this.coercedVariableValues.toMap(), directives, - typeCondition, - newPossibleObjects + (label) -> new NormalizedDeferredExecution(label, newPossibleObjects) ); } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java deleted file mode 100644 index 0daa539329..0000000000 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ /dev/null @@ -1,55 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.Assert; -import graphql.GraphQLContext; -import graphql.Internal; -import graphql.execution.CoercedVariables; -import graphql.execution.ValuesResolver; -import graphql.language.Directive; -import graphql.language.NodeUtil; -import graphql.language.TypeName; -import graphql.schema.GraphQLObjectType; - -import javax.annotation.Nullable; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import static graphql.Directives.DeferDirective; - -@Internal -public class IncrementalNodes { - - public NormalizedDeferredExecution createDeferExecution( - Map variables, - List directives, - @Nullable TypeName targetType, - Set possibleTypes - ) { - Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); - - if (deferDirective != null) { - Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); - - Object flag = argumentValues.get("if"); - Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); - - if (!((Boolean) flag)) { - return null; - } - - Object label = argumentValues.get("label"); - - if (label == null) { - return new NormalizedDeferredExecution(null, possibleTypes); - } - - Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - - return new NormalizedDeferredExecution((String) label, possibleTypes); - } - - return null; - } -} From bbe573a9249e52d02eea5790d851005573f4e9fc Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 15 Feb 2024 11:25:00 +1100 Subject: [PATCH 186/393] Fix one more bug caused by improper usage of field.getName --- .../incremental/DeferredExecutionSupport.java | 6 +- ...eferExecutionSupportIntegrationTest.groovy | 69 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java index c2701741c2..9e24ec2103 100644 --- a/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java +++ b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java @@ -132,7 +132,7 @@ private Supplier fields = new LinkedHashMap<>(); - fields.put(currentField.getName(), currentField); + fields.put(currentField.getResultKey(), currentField); ExecutionStrategyParameters callParameters = parameters.transform(builder -> { @@ -140,7 +140,7 @@ private Supplier child chain - it's a new start effectively } ); @@ -151,7 +151,7 @@ private Supplier FpKit.interThreadMemoize(() -> { diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 62459b52f2..46a0756c6a 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -29,6 +29,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { type Query { post : Post posts: [Post] + postById(id: ID!): Post hello: String item(type: String!): Item } @@ -135,6 +136,9 @@ class DeferExecutionSupportIntegrationTest extends Specification { [id: "1002"], [id: "1003"] ])) + .dataFetcher("postById", (env) -> { + return [id: env.getArgument("id")] + }) .dataFetcher("hello", resolve("world")) .dataFetcher("item", resolveItem()) ) @@ -239,6 +243,71 @@ class DeferExecutionSupportIntegrationTest extends Specification { ] } + def "aliased fields with different parameters"() { + def query = ''' + query { + postById(id: "1") { + id + } + ... @defer { + post2: postById(id: "2") { + id2: id + } + } + ... @defer { + post3: postById(id: "3") { + ... @defer { + id3: id + } + } + } + } + ''' + + when: + IncrementalExecutionResult initialResult = executeQuery(query) + + then: + initialResult.toSpecification() == [ + data : [postById: [id: "1"]], + hasNext: true + ] + + when: + def incrementalResults = getIncrementalResults(initialResult) + + then: + incrementalResults == [ + [ + hasNext : true, + incremental: [ + [ + path: [], + data: [post2: [id2: "2"]] + ] + ] + ], + [ + hasNext : true, + incremental: [ + [ + path: [], + data: [post3: [:]] + ] + ] + ], + [ + hasNext : false, + incremental: [ + [ + path: ["post3"], + data: [id3: "3"] + ] + ] + ] + ] + } + def "defer on interface field"() { def query = """ query { From 37e7bcb4cfac4bd4f48cfd9e734af3ae0a42f830 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 15 Feb 2024 19:58:59 +1000 Subject: [PATCH 187/393] handle applied directive argument deleted object and interface field --- .../diffing/ana/EditOperationAnalyzer.java | 30 ++++++++++ .../schema/diffing/ana/SchemaDifference.java | 16 ++++- ...rationAnalyzerAppliedDirectivesTest.groovy | 60 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index 97e6a324aa..fc262e03ca 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -9,6 +9,7 @@ import graphql.schema.diffing.Mapping; import graphql.schema.diffing.SchemaGraph; import graphql.schema.diffing.Vertex; +import graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentAddition; import graphql.schema.idl.ScalarInfo; import java.util.ArrayList; @@ -212,6 +213,8 @@ private void handleAppliedDirectives(List editOperations, Mapping case INSERT_VERTEX: if (editOperation.getTargetVertex().isOfType(SchemaGraph.APPLIED_DIRECTIVE)) { appliedDirectiveAdded(editOperation); + } else if (editOperation.getTargetVertex().isOfType(SchemaGraph.APPLIED_ARGUMENT)) { + appliedDirectiveArgumentAdded(editOperation); } break; case CHANGE_VERTEX: @@ -341,6 +344,33 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { } } + private void appliedDirectiveArgumentAdded(EditOperation editOperation) { + Vertex addedArgument = editOperation.getTargetVertex(); + Vertex appliedDirective = newSchemaGraph.getAppliedDirectiveForAppliedArgument(addedArgument); + Vertex container = newSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + + if (container.isOfType(SchemaGraph.FIELD)) { + Vertex field = container; + Vertex interfaceOrObjective = newSchemaGraph.getFieldsContainerForField(field); + if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { + Vertex object = interfaceOrObjective; + if (isObjectAdded(object.getName())) { + return; + } + AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName(), appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else { + assertTrue(interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = interfaceOrObjective; + if (isInterfaceAdded(interfaze.getName())) { + return; + } + AppliedDirectiveInterfaceFieldLocation location = new AppliedDirectiveInterfaceFieldLocation(interfaze.getName(), field.getName(), appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } + } + } + private void appliedDirectiveArgumentChanged(EditOperation editOperation) { Vertex appliedArgument = editOperation.getTargetVertex(); String oldArgumentName = editOperation.getSourceVertex().getName(); diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java index 04172d4e91..dbf84ec969 100644 --- a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -1598,8 +1598,22 @@ class AppliedDirectiveRenamed { } - class AppliedDirectiveArgumentAddition { + class AppliedDirectiveArgumentAddition implements ObjectModificationDetail, InterfaceModificationDetail { + private final AppliedDirectiveLocationDetail locationDetail; + private final String argumentName; + public AppliedDirectiveArgumentAddition(AppliedDirectiveLocationDetail locationDetail, String argumentName) { + this.locationDetail = locationDetail; + this.argumentName = argumentName; + } + + public AppliedDirectiveLocationDetail getLocationDetail() { + return locationDetail; + } + + public String getArgumentName() { + return argumentName; + } } class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, InterfaceModificationDetail { diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 623c2dfc04..ff4a5792b0 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -32,6 +32,39 @@ import static graphql.schema.diffing.ana.SchemaDifference.UnionModification class EditOperationAnalyzerAppliedDirectivesTest extends Specification { + def "applied directive argument added interface field"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query implements I{ + foo: String + } + interface I { + foo: String @d + } + ''' + def newSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query implements I{ + foo: String + } + interface I { + foo: String @d(arg1: "foo") + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def argumentAddition = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(SchemaDifference.AppliedDirectiveArgumentAddition) + def location = argumentAddition[0].locationDetail as AppliedDirectiveInterfaceFieldLocation + location.interfaceName == "I" + location.fieldName == "foo" + argumentAddition[0].argumentName == "arg1" + } + def "applied directive argument deleted interface field "() { given: def oldSdl = ''' @@ -177,6 +210,33 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { argumentRenames[0].newName == "arg2" } + def "applied directive argument added object field"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query { + foo: String @d + } + ''' + def newSdl = ''' + directive @d(arg1: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg1: "foo") + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def argumentAddition = (changes.objectDifferences["Query"] as ObjectModification).getDetails(SchemaDifference.AppliedDirectiveArgumentAddition) + def location = argumentAddition[0].locationDetail as AppliedDirectiveObjectFieldLocation + location.objectName == "Query" + location.fieldName == "foo" + argumentAddition[0].argumentName == "arg1" + } + def "applied directive argument deleted object field"() { given: def oldSdl = ''' From cfaa72a53ff68969cb7e691f6a47300163e26b17 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 15 Feb 2024 20:35:16 +1000 Subject: [PATCH 188/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 93 +++++++++++++-- .../schema/diffing/ana/SchemaDifference.java | 4 +- ...rationAnalyzerAppliedDirectivesTest.groovy | 108 +++++++++++++++++- 3 files changed, 193 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index fc262e03ca..22f9d63f2f 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.function.Predicate; +import static graphql.Assert.assertShouldNeverHappen; import static graphql.Assert.assertTrue; import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveAddition; import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentDeletion; @@ -208,12 +209,31 @@ private void handleArgumentChanges(List editOperations, Mapping m private void handleAppliedDirectives(List editOperations, Mapping mapping) { + // first the applied directives itself and then all the applied arguments changes + // for the applied directives, so that we check for example if the applied directive is + // deleted before we check for the applied directive argument changes for (EditOperation editOperation : editOperations) { switch (editOperation.getOperation()) { case INSERT_VERTEX: if (editOperation.getTargetVertex().isOfType(SchemaGraph.APPLIED_DIRECTIVE)) { appliedDirectiveAdded(editOperation); - } else if (editOperation.getTargetVertex().isOfType(SchemaGraph.APPLIED_ARGUMENT)) { + } + break; + case CHANGE_VERTEX: + // TODO: handle applied directive changes + break; + case DELETE_VERTEX: + if (editOperation.getSourceVertex().isOfType(SchemaGraph.APPLIED_DIRECTIVE)) { + appliedDirectiveDeleted(editOperation); + } + break; + + } + } + for (EditOperation editOperation : editOperations) { + switch (editOperation.getOperation()) { + case INSERT_VERTEX: + if (editOperation.getTargetVertex().isOfType(SchemaGraph.APPLIED_ARGUMENT)) { appliedDirectiveArgumentAdded(editOperation); } break; @@ -223,9 +243,7 @@ private void handleAppliedDirectives(List editOperations, Mapping } break; case DELETE_VERTEX: - if (editOperation.getSourceVertex().isOfType(SchemaGraph.APPLIED_DIRECTIVE)) { - appliedDirectiveDeleted(editOperation); - } else if (editOperation.getSourceVertex().isOfType(SchemaGraph.APPLIED_ARGUMENT)) { + if (editOperation.getSourceVertex().isOfType(SchemaGraph.APPLIED_ARGUMENT)) { appliedDirectiveArgumentDeleted(editOperation); } break; @@ -321,6 +339,9 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { Vertex deletedArgument = editOperation.getSourceVertex(); Vertex appliedDirective = oldSchemaGraph.getAppliedDirectiveForAppliedArgument(deletedArgument); Vertex container = oldSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + if (isAppliedDirectiveDeleted(container, appliedDirective.getName())) { + return; + } if (container.isOfType(SchemaGraph.FIELD)) { Vertex field = container; @@ -341,6 +362,20 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { AppliedDirectiveInterfaceFieldLocation location = new AppliedDirectiveInterfaceFieldLocation(interfaze.getName(), field.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } + } else if (container.isOfType(SchemaGraph.SCALAR)) { + Vertex scalar = container; + if (isScalarDeleted(scalar.getName())) { + return; + } + AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName(), appliedDirective.getName()); + getScalarModification(scalar.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (container.isOfType(SchemaGraph.ENUM)) { + Vertex enumVertex = container; + if (isEnumDeleted(enumVertex.getName())) { + return; + } + AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } } @@ -359,15 +394,32 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { } AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName(), appliedDirective.getName()); getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); - } else { - assertTrue(interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)); + } else if (interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)) { Vertex interfaze = interfaceOrObjective; if (isInterfaceAdded(interfaze.getName())) { return; } AppliedDirectiveInterfaceFieldLocation location = new AppliedDirectiveInterfaceFieldLocation(interfaze.getName(), field.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else { + assertShouldNeverHappen("Unexpected field container " + interfaceOrObjective); + } + } else if (container.isOfType(SchemaGraph.SCALAR)) { + Vertex scalar = container; + if (isScalarAdded(scalar.getName())) { + return; + } + AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName(), appliedDirective.getName()); + getScalarModification(scalar.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.ENUM)) { + Vertex enumVertex = container; + if (isEnumAdded(enumVertex.getName())) { + return; } + AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else { +// assertShouldNeverHappen("Unexpected applied argument container " + container); } } @@ -1438,6 +1490,29 @@ private boolean isInputFieldAdded(String name) { return inputObjectDifferences.containsKey(name) && inputObjectDifferences.get(name) instanceof InputObjectAddition; } + private boolean isAppliedDirectiveDeleted(Vertex container, String appliedDirectiveName) { + if (container.isOfType(SchemaGraph.SCALAR)) { + if (scalarDifferences.containsKey(container.getName())) { + ScalarDifference scalarDifference = scalarDifferences.get(container.getName()); + if (scalarDifference instanceof ScalarModification) { + ScalarModification scalarModification = (ScalarModification) scalarDifference; + List appliedDirectiveDeletions = scalarModification.getDetails(AppliedDirectiveDeletion.class); + return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.ENUM)) { + if (enumDifferences.containsKey(container.getName())) { + EnumDifference enumDifference = enumDifferences.get(container.getName()); + if (enumDifference instanceof EnumModification) { + EnumModification enumModification = (EnumModification) enumDifference; + List appliedDirectiveDeletions = enumModification.getDetails(AppliedDirectiveDeletion.class); + return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); + } + } + } + return false; + } + private boolean isNewInputFieldExistingInputObject(String inputObjectName, String fieldName) { if (!inputObjectDifferences.containsKey(inputObjectName)) { return false; @@ -1505,7 +1580,8 @@ private boolean isArgumentNewForExistingObjectField(String objectName, String fi return newArgs.stream().anyMatch(detail -> detail.getFieldName().equals(fieldName) && detail.getName().equals(argumentName)); } - private boolean isArgumentDeletedFromExistingObjectField(String objectName, String fieldName, String argumentName) { + private boolean isArgumentDeletedFromExistingObjectField(String objectName, String fieldName, String + argumentName) { if (!objectDifferences.containsKey(objectName)) { return false; } @@ -1524,7 +1600,8 @@ private boolean isArgumentDeletedFromExistingObjectField(String objectName, Stri return deletedArgs.stream().anyMatch(detail -> detail.getFieldName().equals(fieldName) && detail.getName().equals(argumentName)); } - private boolean isArgumentDeletedFromExistingInterfaceField(String interfaceName, String fieldName, String argumentName) { + private boolean isArgumentDeletedFromExistingInterfaceField(String interfaceName, String fieldName, String + argumentName) { if (!interfaceDifferences.containsKey(interfaceName)) { return false; } diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java index dbf84ec969..97a2215e89 100644 --- a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -1598,7 +1598,7 @@ class AppliedDirectiveRenamed { } - class AppliedDirectiveArgumentAddition implements ObjectModificationDetail, InterfaceModificationDetail { + class AppliedDirectiveArgumentAddition implements ObjectModificationDetail, InterfaceModificationDetail, ScalarModificationDetail, EnumModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; @@ -1616,7 +1616,7 @@ public String getArgumentName() { } } - class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, InterfaceModificationDetail { + class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, InterfaceModificationDetail, ScalarModificationDetail, EnumModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index ff4a5792b0..dadcc6c428 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -5,6 +5,7 @@ import graphql.schema.diffing.SchemaDiffing import spock.lang.Specification import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveAddition +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentAddition import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentDeletion import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentRename import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentValueModification @@ -58,7 +59,7 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.interfaceDifferences["I"] instanceof InterfaceModification - def argumentAddition = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(SchemaDifference.AppliedDirectiveArgumentAddition) + def argumentAddition = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentAddition) def location = argumentAddition[0].locationDetail as AppliedDirectiveInterfaceFieldLocation location.interfaceName == "I" location.fieldName == "foo" @@ -230,7 +231,7 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.objectDifferences["Query"] instanceof ObjectModification - def argumentAddition = (changes.objectDifferences["Query"] as ObjectModification).getDetails(SchemaDifference.AppliedDirectiveArgumentAddition) + def argumentAddition = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentAddition) def location = argumentAddition[0].locationDetail as AppliedDirectiveObjectFieldLocation location.objectName == "Query" location.fieldName == "foo" @@ -616,6 +617,58 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } + def "applied directive deleted argument enum"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM + enum E @d(arg: "foo") { A, B } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM + enum E @d { A, B } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def argumentDeleted = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveArgumentDeletion) + (argumentDeleted[0].locationDetail as AppliedDirectiveEnumLocation).name == "E" + argumentDeleted[0].argumentName == "arg" + } + + def "applied directive added argument enum"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM + enum E @d { A, B } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM + enum E @d(arg: "foo"){ A, B } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def argumentAdded = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveArgumentAddition) + (argumentAdded[0].locationDetail as AppliedDirectiveEnumLocation).name == "E" + argumentAdded[0].argumentName == "arg" + + } + + def "applied directive deleted enum value"() { given: def oldSdl = ''' @@ -876,6 +929,57 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { } + def "applied directive argument added scalar"() { + given: + def oldSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d + type Query { + foo: DateTime + } + ''' + def newSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d(arg: "foo") + type Query { + foo: DateTime + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["DateTime"] instanceof ScalarModification + def argumentAdded = (changes.scalarDifferences["DateTime"] as ScalarModification).getDetails(AppliedDirectiveArgumentAddition) + (argumentAdded[0].locationDetail as AppliedDirectiveScalarLocation).name == "DateTime" + argumentAdded[0].argumentName == "arg" + } + + def "applied directive argument deleted scalar"() { + given: + def oldSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d(arg: "foo") + type Query { + foo: DateTime + } + ''' + def newSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d + type Query { + foo: DateTime + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["DateTime"] instanceof ScalarModification + def argumentDeletion = (changes.scalarDifferences["DateTime"] as ScalarModification).getDetails(AppliedDirectiveArgumentDeletion) + (argumentDeletion[0].locationDetail as AppliedDirectiveScalarLocation).name == "DateTime" + argumentDeletion[0].argumentName == "arg" + } + + EditOperationAnalysisResult calcDiff( String oldSdl, String newSdl From 5c074dca6a041c00c873d1a4607a75438df1af33 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 06:34:23 +1000 Subject: [PATCH 189/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 132 ++++++++++++++++++ .../schema/diffing/ana/SchemaDifference.java | 16 ++- ...rationAnalyzerAppliedDirectivesTest.groovy | 60 ++++++++ 3 files changed, 206 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index 22f9d63f2f..f4d36e0b8d 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -376,6 +376,13 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { } AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (container.isOfType(SchemaGraph.UNION)) { + Vertex union = container; + if (isUnionDeleted(union.getName())) { + return; + } + AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); + getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } } @@ -383,6 +390,9 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { Vertex addedArgument = editOperation.getTargetVertex(); Vertex appliedDirective = newSchemaGraph.getAppliedDirectiveForAppliedArgument(addedArgument); Vertex container = newSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } if (container.isOfType(SchemaGraph.FIELD)) { Vertex field = container; @@ -418,11 +428,19 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { } AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.UNION)) { + Vertex union = container; + if (isUnionAdded(union.getName())) { + return; + } + AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); + getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else { // assertShouldNeverHappen("Unexpected applied argument container " + container); } } + private void appliedDirectiveArgumentChanged(EditOperation editOperation) { Vertex appliedArgument = editOperation.getTargetVertex(); String oldArgumentName = editOperation.getSourceVertex().getName(); @@ -1490,6 +1508,75 @@ private boolean isInputFieldAdded(String name) { return inputObjectDifferences.containsKey(name) && inputObjectDifferences.get(name) instanceof InputObjectAddition; } + private boolean isAppliedDirectiveAdded(Vertex container, String appliedDirectiveName) { + if (container.isOfType(SchemaGraph.SCALAR)) { + if (scalarDifferences.containsKey(container.getName())) { + ScalarDifference scalarDifference = scalarDifferences.get(container.getName()); + if (scalarDifference instanceof ScalarModification) { + ScalarModification scalarModification = (ScalarModification) scalarDifference; + List appliedDirectiveAdditions = scalarModification.getDetails(AppliedDirectiveAddition.class); + return appliedDirectiveAdditions.stream().anyMatch(addition -> addition.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.ENUM)) { + if (enumDifferences.containsKey(container.getName())) { + EnumDifference enumDifference = enumDifferences.get(container.getName()); + if (enumDifference instanceof EnumModification) { + EnumModification enumModification = (EnumModification) enumDifference; + List appliedDirectiveAdditions = enumModification.getDetails(AppliedDirectiveAddition.class); + return appliedDirectiveAdditions.stream().anyMatch(addition -> addition.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.OBJECT)) { + if (objectDifferences.containsKey(container.getName())) { + ObjectDifference objectDifference = objectDifferences.get(container.getName()); + if (objectDifference instanceof ObjectModification) { + ObjectModification objectModification = (ObjectModification) objectDifference; + List appliedDirectiveAdditions = objectModification.getDetails(AppliedDirectiveAddition.class); + return appliedDirectiveAdditions.stream().anyMatch(addition -> addition.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.INTERFACE)) { + if (interfaceDifferences.containsKey(container.getName())) { + InterfaceDifference interfaceDifference = interfaceDifferences.get(container.getName()); + if (interfaceDifference instanceof InterfaceModification) { + InterfaceModification interfaceModification = (InterfaceModification) interfaceDifference; + List appliedDirectiveAdditions = interfaceModification.getDetails(AppliedDirectiveAddition.class); + return appliedDirectiveAdditions.stream().anyMatch(addition -> addition.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + if (inputObjectDifferences.containsKey(container.getName())) { + InputObjectDifference inputObjectDifference = inputObjectDifferences.get(container.getName()); + if (inputObjectDifference instanceof InputObjectModification) { + InputObjectModification inputObjectModification = (InputObjectModification) inputObjectDifference; + List appliedDirectiveAdditions = inputObjectModification.getDetails(AppliedDirectiveAddition.class); + return appliedDirectiveAdditions.stream().anyMatch(addition -> addition.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.UNION)) { + if (unionDifferences.containsKey(container.getName())) { + UnionDifference unionDifference = unionDifferences.get(container.getName()); + if (unionDifference instanceof UnionModification) { + UnionModification unionModification = (UnionModification) unionDifference; + List appliedDirectiveAdditions = unionModification.getDetails(AppliedDirectiveAddition.class); + return appliedDirectiveAdditions.stream().anyMatch(addition -> addition.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.DIRECTIVE)) { + if (directiveDifferences.containsKey(container.getName())) { + DirectiveDifference directiveDifference = directiveDifferences.get(container.getName()); + if (directiveDifference instanceof DirectiveModification) { + DirectiveModification directiveModification = (DirectiveModification) directiveDifference; + List appliedDirectiveAdditions = directiveModification.getDetails(AppliedDirectiveAddition.class); + return appliedDirectiveAdditions.stream().anyMatch(addition -> addition.getName().equals(appliedDirectiveName)); + } + } + } + return false; + } + + private boolean isAppliedDirectiveDeleted(Vertex container, String appliedDirectiveName) { if (container.isOfType(SchemaGraph.SCALAR)) { if (scalarDifferences.containsKey(container.getName())) { @@ -1509,6 +1596,51 @@ private boolean isAppliedDirectiveDeleted(Vertex container, String appliedDirect return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); } } + } else if (container.isOfType(SchemaGraph.OBJECT)) { + if (objectDifferences.containsKey(container.getName())) { + ObjectDifference objectDifference = objectDifferences.get(container.getName()); + if (objectDifference instanceof ObjectModification) { + ObjectModification objectModification = (ObjectModification) objectDifference; + List appliedDirectiveDeletions = objectModification.getDetails(AppliedDirectiveDeletion.class); + return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.INTERFACE)) { + if (interfaceDifferences.containsKey(container.getName())) { + InterfaceDifference interfaceDifference = interfaceDifferences.get(container.getName()); + if (interfaceDifference instanceof InterfaceModification) { + InterfaceModification interfaceModification = (InterfaceModification) interfaceDifference; + List appliedDirectiveDeletions = interfaceModification.getDetails(AppliedDirectiveDeletion.class); + return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + if (inputObjectDifferences.containsKey(container.getName())) { + InputObjectDifference inputObjectDifference = inputObjectDifferences.get(container.getName()); + if (inputObjectDifference instanceof InputObjectModification) { + InputObjectModification inputObjectModification = (InputObjectModification) inputObjectDifference; + List appliedDirectiveDeletions = inputObjectModification.getDetails(AppliedDirectiveDeletion.class); + return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.UNION)) { + if (unionDifferences.containsKey(container.getName())) { + UnionDifference unionDifference = unionDifferences.get(container.getName()); + if (unionDifference instanceof UnionModification) { + UnionModification unionModification = (UnionModification) unionDifference; + List appliedDirectiveDeletions = unionModification.getDetails(AppliedDirectiveDeletion.class); + return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); + } + } + } else if (container.isOfType(SchemaGraph.DIRECTIVE)) { + if (directiveDifferences.containsKey(container.getName())) { + DirectiveDifference directiveDifference = directiveDifferences.get(container.getName()); + if (directiveDifference instanceof DirectiveModification) { + DirectiveModification directiveModification = (DirectiveModification) directiveDifference; + List appliedDirectiveDeletions = directiveModification.getDetails(AppliedDirectiveDeletion.class); + return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); + } + } } return false; } diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java index 97a2215e89..18f25b42d0 100644 --- a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -1450,6 +1450,10 @@ public AppliedDirectiveUnionLocation(String name, String directiveName) { public String getName() { return name; } + + public String getDirectiveName() { + return directiveName; + } } class AppliedDirectiveEnumLocation implements AppliedDirectiveLocationDetail { @@ -1598,7 +1602,11 @@ class AppliedDirectiveRenamed { } - class AppliedDirectiveArgumentAddition implements ObjectModificationDetail, InterfaceModificationDetail, ScalarModificationDetail, EnumModificationDetail { + class AppliedDirectiveArgumentAddition implements ObjectModificationDetail, + InterfaceModificationDetail, + ScalarModificationDetail, + EnumModificationDetail, + UnionModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; @@ -1616,7 +1624,11 @@ public String getArgumentName() { } } - class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, InterfaceModificationDetail, ScalarModificationDetail, EnumModificationDetail { + class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, + InterfaceModificationDetail, + ScalarModificationDetail, + EnumModificationDetail, + UnionModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index dadcc6c428..5bd22aa3dc 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -928,6 +928,66 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } + def "applied directive argument added union"() { + given: + def oldSdl = ''' + directive @d(arg:String) on UNION + type Query { + foo: FooBar + } + union FooBar @d = A | B + type A { a: String } + type B { b: String } + ''' + def newSdl = ''' + directive @d(arg:String) on UNION + type Query { + foo: FooBar + } + union FooBar @d(arg:"arg") = A | B + type A { a: String } + type B { b: String } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["FooBar"] instanceof UnionModification + def argumentAdded = (changes.unionDifferences["FooBar"] as UnionModification).getDetails(AppliedDirectiveArgumentAddition) + (argumentAdded[0].locationDetail as AppliedDirectiveUnionLocation).name == "FooBar" + (argumentAdded[0].locationDetail as AppliedDirectiveUnionLocation).directiveName == "d" + argumentAdded[0].argumentName == "arg" + } + + def "applied directive argument deleted union"() { + given: + def oldSdl = ''' + directive @d(arg:String) on UNION + type Query { + foo: FooBar + } + union FooBar @d(arg:"arg") = A | B + type A { a: String } + type B { b: String } + ''' + def newSdl = ''' + directive @d(arg:String) on UNION + type Query { + foo: FooBar + } + union FooBar @d = A | B + type A { a: String } + type B { b: String } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["FooBar"] instanceof UnionModification + def argumentDeleted = (changes.unionDifferences["FooBar"] as UnionModification).getDetails(AppliedDirectiveArgumentDeletion) + (argumentDeleted[0].locationDetail as AppliedDirectiveUnionLocation).name == "FooBar" + (argumentDeleted[0].locationDetail as AppliedDirectiveUnionLocation).directiveName == "d" + argumentDeleted[0].argumentName == "arg" + } + def "applied directive argument added scalar"() { given: From 047bc00db7666a6398d8b23fb26e5dc72cd4b445 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 06:42:59 +1000 Subject: [PATCH 190/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 14 +++++ .../schema/diffing/ana/SchemaDifference.java | 6 +- ...rationAnalyzerAppliedDirectivesTest.groovy | 58 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index f4d36e0b8d..81069087b5 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -383,6 +383,13 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { } AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + Vertex inputObject = container; + if (isInputObjectDeleted(inputObject.getName())) { + return; + } + AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName(), appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } } @@ -435,6 +442,13 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { } AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + Vertex inputObject = container; + if (isInputObjectAdded(inputObject.getName())) { + return; + } + AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName(), appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else { // assertShouldNeverHappen("Unexpected applied argument container " + container); } diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java index 18f25b42d0..93b645103d 100644 --- a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -1606,7 +1606,8 @@ class AppliedDirectiveArgumentAddition implements ObjectModificationDetail, InterfaceModificationDetail, ScalarModificationDetail, EnumModificationDetail, - UnionModificationDetail { + UnionModificationDetail, + InputObjectModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; @@ -1628,7 +1629,8 @@ class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, InterfaceModificationDetail, ScalarModificationDetail, EnumModificationDetail, - UnionModificationDetail { + UnionModificationDetail, + InputObjectModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 5bd22aa3dc..30fde5797c 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -1039,6 +1039,64 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { argumentDeletion[0].argumentName == "arg" } + def "applied directive argument added input object"() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d { + a: String + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d(arg: "foo") { + a: String + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def argumentAdded = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveArgumentAddition) + (argumentAdded[0].locationDetail as AppliedDirectiveInputObjectLocation).name == "I" + argumentAdded[0].argumentName == "arg" + } + + def "applied directive argument deleted input object"() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d(arg: "foo") { + a: String + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d { + a: String + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def argumentAdded = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveArgumentDeletion) + (argumentAdded[0].locationDetail as AppliedDirectiveInputObjectLocation).name == "I" + argumentAdded[0].argumentName == "arg" + } + EditOperationAnalysisResult calcDiff( String oldSdl, From 9ec45f87fa3c8f312841bb4dcc65b0c68accd14f Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 06:57:19 +1000 Subject: [PATCH 191/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 28 +++++++++- ...rationAnalyzerAppliedDirectivesTest.groovy | 52 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index 81069087b5..ab73493858 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -343,7 +343,19 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { return; } - if (container.isOfType(SchemaGraph.FIELD)) { + if (container.isOfType(SchemaGraph.ARGUMENT)) { + Vertex argument = container; + Vertex fieldOrDirective = oldSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex fieldsContainer = oldSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainer.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainer; + AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName(), appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } + } + } else if (container.isOfType(SchemaGraph.FIELD)) { Vertex field = container; Vertex interfaceOrObjective = oldSchemaGraph.getFieldsContainerForField(field); if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { @@ -401,7 +413,19 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { return; } - if (container.isOfType(SchemaGraph.FIELD)) { + if (container.isOfType(SchemaGraph.ARGUMENT)) { + Vertex argument = container; + Vertex fieldOrDirective = newSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex fieldsContainer = newSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainer.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainer; + AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName(), appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } + } + } else if (container.isOfType(SchemaGraph.FIELD)) { Vertex field = container; Vertex interfaceOrObjective = newSchemaGraph.getFieldsContainerForField(field); if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 30fde5797c..8f163c42d3 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -480,6 +480,58 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } + def "applied directive argument added object field argument"() { + given: + def oldSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d) : String + } + ''' + def newSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d(directiveArg: "foo")) : String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirectiveArgumentAddition = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentAddition) + def locationDetail = appliedDirectiveArgumentAddition[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation + locationDetail.objectName == "Query" + locationDetail.fieldName == "foo" + locationDetail.argumentName == "arg" + appliedDirectiveArgumentAddition[0].argumentName == "directiveArg" + } + + def "applied directive argument deleted object field argument"() { + given: + def oldSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d(directiveArg: "foo")) : String + } + ''' + def newSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d) : String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirectiveArgumentDeletion = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentDeletion) + def locationDetail = appliedDirectiveArgumentDeletion[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation + locationDetail.objectName == "Query" + locationDetail.fieldName == "foo" + locationDetail.argumentName == "arg" + appliedDirectiveArgumentDeletion[0].argumentName == "directiveArg" + } + def "applied directive added interface field argument"() { given: def oldSdl = ''' From 7d6249cb45eeaffea50b19f3b1b4ab0e18e704f4 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 07:02:42 +1000 Subject: [PATCH 192/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 8 +++ ...rationAnalyzerAppliedDirectivesTest.groovy | 64 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index ab73493858..3cf96d3659 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -353,6 +353,10 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { Vertex object = fieldsContainer; AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (fieldsContainer.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = fieldsContainer; + AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName(), appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } } } else if (container.isOfType(SchemaGraph.FIELD)) { @@ -423,6 +427,10 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { Vertex object = fieldsContainer; AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (fieldsContainer.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = fieldsContainer; + AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName(), appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } } } else if (container.isOfType(SchemaGraph.FIELD)) { diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 8f163c42d3..93bec87ef1 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -563,6 +563,70 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } + def "applied directive argument added interface field argument"() { + given: + def oldSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d): String + } + ''' + def newSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d(directiveArg: "foo") ): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def appliedDirective = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentAddition) + def location = appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation + location.interfaceName == "I" + location.fieldName == "foo" + location.argumentName == "arg" + appliedDirective[0].argumentName == "directiveArg" + } + + def "applied directive argument deleted interface field argument"() { + given: + def oldSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d(directiveArg: "foo")): String + } + ''' + def newSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def appliedDirective = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation + location.interfaceName == "I" + location.fieldName == "foo" + location.argumentName == "arg" + appliedDirective[0].argumentName == "directiveArg" + } + def "applied directive added directive argument "() { given: def oldSdl = ''' From a864f486a40ed06539b037f6af55d9a522e4a360 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 07:14:07 +1000 Subject: [PATCH 193/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 8 +++ .../schema/diffing/ana/SchemaDifference.java | 6 +- ...rationAnalyzerAppliedDirectivesTest.groovy | 55 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index 3cf96d3659..e4eb180342 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -358,6 +358,10 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } + } else if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { + Vertex directive = fieldOrDirective; + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(appliedDirective.getName(), argument.getName()); + getDirectiveModification(directive.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } } else if (container.isOfType(SchemaGraph.FIELD)) { Vertex field = container; @@ -432,6 +436,10 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } + } else if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { + Vertex directive = fieldOrDirective; + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(appliedDirective.getName(), argument.getName()); + getDirectiveModification(directive.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } } else if (container.isOfType(SchemaGraph.FIELD)) { Vertex field = container; diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java index 93b645103d..0436ac9e5f 100644 --- a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -1607,7 +1607,8 @@ class AppliedDirectiveArgumentAddition implements ObjectModificationDetail, ScalarModificationDetail, EnumModificationDetail, UnionModificationDetail, - InputObjectModificationDetail { + InputObjectModificationDetail, + DirectiveModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; @@ -1630,7 +1631,8 @@ class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, ScalarModificationDetail, EnumModificationDetail, UnionModificationDetail, - InputObjectModificationDetail { + InputObjectModificationDetail, + DirectiveModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 93bec87ef1..67aef9d3c3 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -653,6 +653,61 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } + def "applied directive argument added directive argument "() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg2:String @d) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg2:String @d(arg:"foo") ) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d2"] instanceof DirectiveModification + def appliedDirectiveArgumentAddition = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveArgumentAddition) + def location = appliedDirectiveArgumentAddition[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation + location.directiveName == "d" + location.argumentName == "arg2" + appliedDirectiveArgumentAddition[0].argumentName == "arg" + } + + def "applied directive argument deleted directive argument "() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg2:String @d(arg:"foo")) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg2:String @d ) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d2"] instanceof DirectiveModification + def appliedDirectiveArgumentDeletion = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = appliedDirectiveArgumentDeletion[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation + location.directiveName == "d" + location.argumentName == "arg2" + appliedDirectiveArgumentDeletion[0].argumentName == "arg" + } + + def "applied directive deleted object"() { given: def oldSdl = ''' From 96c4f444f669db6d9c9f38bd115b7de2b58cd826 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 07:53:14 +1000 Subject: [PATCH 194/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 118 ++++++++++++++---- ...rationAnalyzerAppliedDirectivesTest.groovy | 12 +- 2 files changed, 101 insertions(+), 29 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index e4eb180342..36b9434b91 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -339,6 +339,7 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { Vertex deletedArgument = editOperation.getSourceVertex(); Vertex appliedDirective = oldSchemaGraph.getAppliedDirectiveForAppliedArgument(deletedArgument); Vertex container = oldSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + // TODO: container should be a root container (e.g. Interface) not Argument or Field if (isAppliedDirectiveDeleted(container, appliedDirective.getName())) { return; } @@ -351,15 +352,24 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { Vertex fieldsContainer = oldSchemaGraph.getFieldsContainerForField(field); if (fieldsContainer.isOfType(SchemaGraph.OBJECT)) { Vertex object = fieldsContainer; + if (isAppliedDirectiveDeleted(object, appliedDirective.getName())) { + return; + } AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else if (fieldsContainer.isOfType(SchemaGraph.INTERFACE)) { Vertex interfaze = fieldsContainer; + if (isAppliedDirectiveDeleted(interfaze, appliedDirective.getName())) { + return; + } AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } } else if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { Vertex directive = fieldOrDirective; + if (isAppliedDirectiveDeleted(fieldOrDirective, appliedDirective.getName())) { + return; + } AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(appliedDirective.getName(), argument.getName()); getDirectiveModification(directive.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } @@ -417,9 +427,6 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { Vertex addedArgument = editOperation.getTargetVertex(); Vertex appliedDirective = newSchemaGraph.getAppliedDirectiveForAppliedArgument(addedArgument); Vertex container = newSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); - if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { - return; - } if (container.isOfType(SchemaGraph.ARGUMENT)) { Vertex argument = container; @@ -429,15 +436,43 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { Vertex fieldsContainer = newSchemaGraph.getFieldsContainerForField(field); if (fieldsContainer.isOfType(SchemaGraph.OBJECT)) { Vertex object = fieldsContainer; + if (isObjectAdded(object.getName())) { + return; + } + if (isFieldNewForExistingObject(object.getName(), field.getName())) { + return; + } + if (isArgumentNewForExistingObjectField(object.getName(), field.getName(), argument.getName())) { + return; + } + if (isAppliedDirectiveAdded(object, appliedDirective.getName())) { + return; + } AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else if (fieldsContainer.isOfType(SchemaGraph.INTERFACE)) { Vertex interfaze = fieldsContainer; + if (isInterfaceAdded(interfaze.getName())) { + return; + } + if (isFieldNewForExistingInterface(interfaze.getName(), field.getName())) { + return; + } + if (isArgumentNewForExistingInterfaceField(interfaze.getName(), field.getName(), argument.getName())) { + return; + } + + if (isAppliedDirectiveAdded(interfaze, appliedDirective.getName())) { + return; + } AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } } else if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { Vertex directive = fieldOrDirective; + if (isAppliedDirectiveAdded(directive, appliedDirective.getName())) { + return; + } AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(appliedDirective.getName(), argument.getName()); getDirectiveModification(directive.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } @@ -449,6 +484,13 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { if (isObjectAdded(object.getName())) { return; } + if (isFieldNewForExistingObject(object.getName(), field.getName())) { + return; + } + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } + AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName(), appliedDirective.getName()); getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else if (interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)) { @@ -456,12 +498,22 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { if (isInterfaceAdded(interfaze.getName())) { return; } + if (isFieldNewForExistingInterface(interfaze.getName(), field.getName())) { + return; + } + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } + AppliedDirectiveInterfaceFieldLocation location = new AppliedDirectiveInterfaceFieldLocation(interfaze.getName(), field.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else { assertShouldNeverHappen("Unexpected field container " + interfaceOrObjective); } } else if (container.isOfType(SchemaGraph.SCALAR)) { + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } Vertex scalar = container; if (isScalarAdded(scalar.getName())) { return; @@ -469,6 +521,9 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName(), appliedDirective.getName()); getScalarModification(scalar.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else if (container.isOfType(SchemaGraph.ENUM)) { + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } Vertex enumVertex = container; if (isEnumAdded(enumVertex.getName())) { return; @@ -476,6 +531,9 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else if (container.isOfType(SchemaGraph.UNION)) { + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } Vertex union = container; if (isUnionAdded(union.getName())) { return; @@ -483,6 +541,9 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } Vertex inputObject = container; if (isInputObjectAdded(inputObject.getName())) { return; @@ -1631,64 +1692,71 @@ private boolean isAppliedDirectiveAdded(Vertex container, String appliedDirectiv } - private boolean isAppliedDirectiveDeleted(Vertex container, String appliedDirectiveName) { - if (container.isOfType(SchemaGraph.SCALAR)) { - if (scalarDifferences.containsKey(container.getName())) { - ScalarDifference scalarDifference = scalarDifferences.get(container.getName()); + private boolean isAppliedDirectiveDeleted(Vertex rootContainer, String appliedDirectiveName) { +// if (rootContainer.isOfType(SchemaGraph.ARGUMENT)) { +// Vertex argument = rootContainer; +// Vertex fieldOrDirective = oldSchemaGraph.getFieldOrDirectiveForArgument(argument); +// if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { +// return isArgumentDeletedFromExistingDirective(fieldOrDirective.getName(), argument.getName()); +// } +// } + if (rootContainer.isOfType(SchemaGraph.SCALAR)) { + if (scalarDifferences.containsKey(rootContainer.getName())) { + ScalarDifference scalarDifference = scalarDifferences.get(rootContainer.getName()); if (scalarDifference instanceof ScalarModification) { ScalarModification scalarModification = (ScalarModification) scalarDifference; List appliedDirectiveDeletions = scalarModification.getDetails(AppliedDirectiveDeletion.class); return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); } } - } else if (container.isOfType(SchemaGraph.ENUM)) { - if (enumDifferences.containsKey(container.getName())) { - EnumDifference enumDifference = enumDifferences.get(container.getName()); + } else if (rootContainer.isOfType(SchemaGraph.ENUM)) { + if (enumDifferences.containsKey(rootContainer.getName())) { + EnumDifference enumDifference = enumDifferences.get(rootContainer.getName()); if (enumDifference instanceof EnumModification) { EnumModification enumModification = (EnumModification) enumDifference; List appliedDirectiveDeletions = enumModification.getDetails(AppliedDirectiveDeletion.class); return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); } } - } else if (container.isOfType(SchemaGraph.OBJECT)) { - if (objectDifferences.containsKey(container.getName())) { - ObjectDifference objectDifference = objectDifferences.get(container.getName()); + } else if (rootContainer.isOfType(SchemaGraph.OBJECT)) { + if (objectDifferences.containsKey(rootContainer.getName())) { + ObjectDifference objectDifference = objectDifferences.get(rootContainer.getName()); if (objectDifference instanceof ObjectModification) { ObjectModification objectModification = (ObjectModification) objectDifference; List appliedDirectiveDeletions = objectModification.getDetails(AppliedDirectiveDeletion.class); return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); } } - } else if (container.isOfType(SchemaGraph.INTERFACE)) { - if (interfaceDifferences.containsKey(container.getName())) { - InterfaceDifference interfaceDifference = interfaceDifferences.get(container.getName()); + } else if (rootContainer.isOfType(SchemaGraph.INTERFACE)) { + if (interfaceDifferences.containsKey(rootContainer.getName())) { + InterfaceDifference interfaceDifference = interfaceDifferences.get(rootContainer.getName()); if (interfaceDifference instanceof InterfaceModification) { InterfaceModification interfaceModification = (InterfaceModification) interfaceDifference; List appliedDirectiveDeletions = interfaceModification.getDetails(AppliedDirectiveDeletion.class); return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); } } - } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { - if (inputObjectDifferences.containsKey(container.getName())) { - InputObjectDifference inputObjectDifference = inputObjectDifferences.get(container.getName()); + } else if (rootContainer.isOfType(SchemaGraph.INPUT_OBJECT)) { + if (inputObjectDifferences.containsKey(rootContainer.getName())) { + InputObjectDifference inputObjectDifference = inputObjectDifferences.get(rootContainer.getName()); if (inputObjectDifference instanceof InputObjectModification) { InputObjectModification inputObjectModification = (InputObjectModification) inputObjectDifference; List appliedDirectiveDeletions = inputObjectModification.getDetails(AppliedDirectiveDeletion.class); return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); } } - } else if (container.isOfType(SchemaGraph.UNION)) { - if (unionDifferences.containsKey(container.getName())) { - UnionDifference unionDifference = unionDifferences.get(container.getName()); + } else if (rootContainer.isOfType(SchemaGraph.UNION)) { + if (unionDifferences.containsKey(rootContainer.getName())) { + UnionDifference unionDifference = unionDifferences.get(rootContainer.getName()); if (unionDifference instanceof UnionModification) { UnionModification unionModification = (UnionModification) unionDifference; List appliedDirectiveDeletions = unionModification.getDetails(AppliedDirectiveDeletion.class); return appliedDirectiveDeletions.stream().anyMatch(deletion -> deletion.getName().equals(appliedDirectiveName)); } } - } else if (container.isOfType(SchemaGraph.DIRECTIVE)) { - if (directiveDifferences.containsKey(container.getName())) { - DirectiveDifference directiveDifference = directiveDifferences.get(container.getName()); + } else if (rootContainer.isOfType(SchemaGraph.DIRECTIVE)) { + if (directiveDifferences.containsKey(rootContainer.getName())) { + DirectiveDifference directiveDifference = directiveDifferences.get(rootContainer.getName()); if (directiveDifference instanceof DirectiveModification) { DirectiveModification directiveModification = (DirectiveModification) directiveDifference; List appliedDirectiveDeletions = directiveModification.getDetails(AppliedDirectiveDeletion.class); diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 67aef9d3c3..e124cb95bf 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -673,6 +673,7 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.directiveDifferences["d2"] instanceof DirectiveModification + (changes.directiveDifferences["d2"] as DirectiveModification).details.size() == 1 def appliedDirectiveArgumentAddition = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveArgumentAddition) def location = appliedDirectiveArgumentAddition[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation location.directiveName == "d" @@ -700,6 +701,7 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.directiveDifferences["d2"] instanceof DirectiveModification + (changes.directiveDifferences["d2"] as DirectiveModification).details.size() == 1 def appliedDirectiveArgumentDeletion = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveArgumentDeletion) def location = appliedDirectiveArgumentDeletion[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation location.directiveName == "d" @@ -733,17 +735,17 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } - def "applied directive deleted directive argument "() { + def "applied directive deleted argument directive"() { given: def oldSdl = ''' - directive @d(arg:String) on ARGUMENT_DEFINITION - directive @d2(arg:String @d) on ARGUMENT_DEFINITION + directive @d(arg1:String) on ARGUMENT_DEFINITION + directive @d2(arg:String @d(arg1:"foo")) on ARGUMENT_DEFINITION type Query { foo: String } ''' def newSdl = ''' - directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d(arg1:String) on ARGUMENT_DEFINITION directive @d2(arg:String) on ARGUMENT_DEFINITION type Query { foo: String @@ -753,6 +755,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.directiveDifferences["d2"] instanceof DirectiveModification + // whole applied directive is deleted, so we don't count the applied argument deletion + (changes.directiveDifferences["d2"] as DirectiveModification).details.size() == 1 def appliedDirective = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveDeletion) (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).directiveName == "d2" (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).argumentName == "arg" From 1c71de1e3d46ca022c3980e58a802f399e97e3df Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 08:00:48 +1000 Subject: [PATCH 195/393] more handling of applied argument cases --- .../diffing/ana/EditOperationAnalyzer.java | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index 36b9434b91..b5a51c1f64 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -339,10 +339,6 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { Vertex deletedArgument = editOperation.getSourceVertex(); Vertex appliedDirective = oldSchemaGraph.getAppliedDirectiveForAppliedArgument(deletedArgument); Vertex container = oldSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); - // TODO: container should be a root container (e.g. Interface) not Argument or Field - if (isAppliedDirectiveDeleted(container, appliedDirective.getName())) { - return; - } if (container.isOfType(SchemaGraph.ARGUMENT)) { Vertex argument = container; @@ -352,6 +348,15 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { Vertex fieldsContainer = oldSchemaGraph.getFieldsContainerForField(field); if (fieldsContainer.isOfType(SchemaGraph.OBJECT)) { Vertex object = fieldsContainer; + if (isObjectDeleted(object.getName())) { + return; + } + if (isFieldDeletedFromExistingObject(object.getName(), field.getName())) { + return; + } + if (isArgumentDeletedFromExistingObjectField(object.getName(), field.getName(), argument.getName())) { + return; + } if (isAppliedDirectiveDeleted(object, appliedDirective.getName())) { return; } @@ -359,6 +364,15 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else if (fieldsContainer.isOfType(SchemaGraph.INTERFACE)) { Vertex interfaze = fieldsContainer; + if (isInterfaceDeleted(interfaze.getName())) { + return; + } + if (isFieldNewForExistingInterface(interfaze.getName(), field.getName())) { + return; + } + if (isArgumentDeletedFromExistingInterfaceField(interfaze.getName(), field.getName(), argument.getName())) { + return; + } if (isAppliedDirectiveDeleted(interfaze, appliedDirective.getName())) { return; } @@ -367,6 +381,12 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { } } else if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { Vertex directive = fieldOrDirective; + if (isDirectiveDeleted(directive.getName())) { + return; + } + if (isArgumentDeletedFromExistingDirective(directive.getName(), argument.getName())) { + return; + } if (isAppliedDirectiveDeleted(fieldOrDirective, appliedDirective.getName())) { return; } @@ -381,6 +401,13 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { if (isObjectDeleted(object.getName())) { return; } + if (isFieldDeletedFromExistingObject(object.getName(), field.getName())) { + return; + } + if (isAppliedDirectiveDeleted(object, appliedDirective.getName())) { + return; + } + AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName(), appliedDirective.getName()); getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else { @@ -389,6 +416,13 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { if (isInterfaceDeleted(interfaze.getName())) { return; } + if (isFieldDeletedFromExistingInterface(interfaze.getName(), field.getName())) { + return; + } + if (isAppliedDirectiveDeleted(interfaze, appliedDirective.getName())) { + return; + } + AppliedDirectiveInterfaceFieldLocation location = new AppliedDirectiveInterfaceFieldLocation(interfaze.getName(), field.getName(), appliedDirective.getName()); getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } @@ -397,6 +431,10 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { if (isScalarDeleted(scalar.getName())) { return; } + if (isAppliedDirectiveDeleted(scalar, appliedDirective.getName())) { + return; + } + AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName(), appliedDirective.getName()); getScalarModification(scalar.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else if (container.isOfType(SchemaGraph.ENUM)) { @@ -404,6 +442,9 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { if (isEnumDeleted(enumVertex.getName())) { return; } + if (isAppliedDirectiveDeleted(enumVertex, appliedDirective.getName())) { + return; + } AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else if (container.isOfType(SchemaGraph.UNION)) { @@ -411,6 +452,10 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { if (isUnionDeleted(union.getName())) { return; } + if (isAppliedDirectiveDeleted(union, appliedDirective.getName())) { + return; + } + AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { @@ -418,9 +463,13 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { if (isInputObjectDeleted(inputObject.getName())) { return; } + if (isAppliedDirectiveDeleted(inputObject, appliedDirective.getName())) { + return; + } AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName(), appliedDirective.getName()); getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } + //TODO: ENum values and input fields } private void appliedDirectiveArgumentAdded(EditOperation editOperation) { @@ -553,6 +602,8 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { } else { // assertShouldNeverHappen("Unexpected applied argument container " + container); } + // TODO: ENUM Values + // TODO: INput fields } From f36dc74f469c5e73e5376f81d383465e3ad6854a Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 10:03:46 +1000 Subject: [PATCH 196/393] wip --- .../diffing/ana/EditOperationAnalyzer.java | 94 +++++++-- ...rationAnalyzerAppliedDirectivesTest.groovy | 182 ++++++++++++++++++ 2 files changed, 264 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index b5a51c1f64..86ef69c635 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -447,6 +447,17 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { } AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (container.isOfType(SchemaGraph.ENUM_VALUE)) { + Vertex enumValue = container; + Vertex enumVertex = oldSchemaGraph.getEnumForEnumValue(enumValue); + if (isEnumDeleted(enumVertex.getName())) { + return; + } + if (isNewEnumValueForExistingEnum(enumVertex.getName(), enumValue.getName())) { + return; + } + AppliedDirectiveEnumValueLocation location = new AppliedDirectiveEnumValueLocation(enumVertex.getName(), enumValue.getName(), appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else if (container.isOfType(SchemaGraph.UNION)) { Vertex union = container; if (isUnionDeleted(union.getName())) { @@ -468,8 +479,21 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { } AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName(), appliedDirective.getName()); getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (container.isOfType(SchemaGraph.INPUT_FIELD)) { + Vertex inputField = container; + Vertex inputObject = oldSchemaGraph.getInputObjectForInputField(inputField); + if (isInputObjectDeleted(inputObject.getName())) { + return; + } + if (isNewInputFieldExistingInputObject(inputObject.getName(), inputField.getName())) { + return; + } + if (isAppliedDirectiveDeleted(inputField, appliedDirective.getName())) { + return; + } + AppliedDirectiveInputObjectFieldLocation location = new AppliedDirectiveInputObjectFieldLocation(inputObject.getName(), inputField.getName(), appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } - //TODO: ENum values and input fields } private void appliedDirectiveArgumentAdded(EditOperation editOperation) { @@ -560,50 +584,96 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { assertShouldNeverHappen("Unexpected field container " + interfaceOrObjective); } } else if (container.isOfType(SchemaGraph.SCALAR)) { - if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { - return; - } Vertex scalar = container; if (isScalarAdded(scalar.getName())) { return; } - AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName(), appliedDirective.getName()); - getScalarModification(scalar.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); - } else if (container.isOfType(SchemaGraph.ENUM)) { if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { return; } + AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName(), appliedDirective.getName()); + getScalarModification(scalar.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.ENUM)) { Vertex enumVertex = container; if (isEnumAdded(enumVertex.getName())) { return; } + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); - } else if (container.isOfType(SchemaGraph.UNION)) { + } else if (container.isOfType(SchemaGraph.ENUM_VALUE)) { + Vertex enumValue = container; + Vertex enumVertex = newSchemaGraph.getEnumForEnumValue(enumValue); + if (isEnumAdded(enumVertex.getName())) { + return; + } + if (isNewEnumValueForExistingEnum(enumVertex.getName(), enumValue.getName())) { + return; + } if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { return; } + AppliedDirectiveEnumValueLocation location = new AppliedDirectiveEnumValueLocation(enumVertex.getName(), enumValue.getName(), appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.UNION)) { Vertex union = container; if (isUnionAdded(union.getName())) { return; } + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); - } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + } else if (container.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = container; + if (isInterfaceAdded(interfaze.getName())) { + return; + } + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } + AppliedDirectiveInterfaceLocation location = new AppliedDirectiveInterfaceLocation(interfaze.getName(), appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.OBJECT)) { + Vertex interfaze = container; + if (isObjectAdded(interfaze.getName())) { + return; + } if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { return; } + AppliedDirectiveObjectLocation location = new AppliedDirectiveObjectLocation(interfaze.getName(), appliedDirective.getName()); + getObjectModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { Vertex inputObject = container; if (isInputObjectAdded(inputObject.getName())) { return; } + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName(), appliedDirective.getName()); getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); + } else if (container.isOfType(SchemaGraph.INPUT_FIELD)) { + Vertex inputField = container; + Vertex inputObject = newSchemaGraph.getInputObjectForInputField(inputField); + if (isInputObjectAdded(inputObject.getName())) { + return; + } + if (isNewInputFieldExistingInputObject(inputObject.getName(), inputField.getName())) { + return; + } + if (isAppliedDirectiveAdded(container, appliedDirective.getName())) { + return; + } + AppliedDirectiveInputObjectFieldLocation location = new AppliedDirectiveInputObjectFieldLocation(inputObject.getName(), inputField.getName(), appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } else { -// assertShouldNeverHappen("Unexpected applied argument container " + container); + assertShouldNeverHappen("Unexpected applied argument container " + container); } - // TODO: ENUM Values - // TODO: INput fields } diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index e124cb95bf..7f605f1980 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -66,6 +66,38 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { argumentAddition[0].argumentName == "arg1" } + def "applied directive argument added interface"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on INTERFACE + + type Query implements I{ + foo: String + } + interface I @d { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg1:String) on INTERFACE + + type Query implements I{ + foo: String + } + interface I @d(arg1: "foo") { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def argumentAddition = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentAddition) + def location = argumentAddition[0].locationDetail as AppliedDirectiveInterfaceLocation + location.name == "I" + argumentAddition[0].argumentName == "arg1" + } + def "applied directive argument deleted interface field "() { given: def oldSdl = ''' @@ -211,6 +243,32 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { argumentRenames[0].newName == "arg2" } + def "applied directive argument added object"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on OBJECT + + type Query @d { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg1: String) on OBJECT + + type Query @d(arg1: "foo") { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def argumentAddition = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentAddition) + def location = argumentAddition[0].locationDetail as AppliedDirectiveObjectLocation + location.name == "Query" + argumentAddition[0].argumentName == "arg1" + } + def "applied directive argument added object field"() { given: def oldSdl = ''' @@ -314,6 +372,7 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.objectDifferences["Query"] instanceof ObjectModification + (changes.objectDifferences as ObjectModification).details.size() == 1 def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveAddition) (appliedDirective[0].locationDetail as AppliedDirectiveObjectLocation).name == "Query" appliedDirective[0].name == "d" @@ -455,6 +514,63 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } + def "applied directive argument added enum value"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d(arg: "foo") } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def argumentAdded = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveArgumentAddition) + def location = argumentAdded[0].locationDetail as AppliedDirectiveEnumValueLocation + location.enumName == "E" + location.valueName == "B" + location.directiveName == "d" + argumentAdded[0].argumentName == "arg" + } + + def "applied directive argument deleted enum value"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d(arg: "foo") } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def argumentDeletion = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = argumentDeletion[0].locationDetail as AppliedDirectiveEnumValueLocation + location.enumName == "E" + location.valueName == "B" + location.directiveName == "d" + argumentDeletion[0].argumentName == "arg" + } + + def "applied directive added object field argument"() { given: def oldSdl = ''' @@ -930,6 +1046,72 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } + + def "applied directive argument added input object field "() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d(arg: "foo") + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def argumentAdded = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveArgumentAddition) + def location = argumentAdded[0].locationDetail as AppliedDirectiveInputObjectFieldLocation + location.inputObjectName == "I" + location.fieldName == "a" + location.directiveName == "d" + argumentAdded[0].argumentName == "arg" + } + + def "applied directive argument deleted input object field "() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d(arg: "foo") + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def argumentDeletion = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = argumentDeletion[0].locationDetail as AppliedDirectiveInputObjectFieldLocation + location.inputObjectName == "I" + location.fieldName == "a" + location.directiveName == "d" + argumentDeletion[0].argumentName == "arg" + } + + def "applied directive deleted interface"() { given: def oldSdl = ''' From 9814893d514f37bdb8f85d1e71cc0e1b00b6be41 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 10:12:11 +1000 Subject: [PATCH 197/393] wip --- .../diffing/ana/EditOperationAnalyzer.java | 24 ++++++++ ...rationAnalyzerAppliedDirectivesTest.groovy | 61 ++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index 86ef69c635..1edd0840c4 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -469,6 +469,28 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); getUnionModification(union.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (container.isOfType(SchemaGraph.OBJECT)) { + Vertex object = container; + if (isObjectDeleted(object.getName())) { + return; + } + if (isAppliedDirectiveDeleted(object, appliedDirective.getName())) { + return; + } + + AppliedDirectiveObjectLocation location = new AppliedDirectiveObjectLocation(object.getName(), appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else if (container.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = container; + if (isInterfaceDeleted(interfaze.getName())) { + return; + } + if (isAppliedDirectiveDeleted(interfaze, appliedDirective.getName())) { + return; + } + + AppliedDirectiveInterfaceLocation location = new AppliedDirectiveInterfaceLocation(interfaze.getName(), appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { Vertex inputObject = container; if (isInputObjectDeleted(inputObject.getName())) { @@ -493,6 +515,8 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { } AppliedDirectiveInputObjectFieldLocation location = new AppliedDirectiveInputObjectFieldLocation(inputObject.getName(), inputField.getName(), appliedDirective.getName()); getInputObjectModification(inputObject.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else { + assertShouldNeverHappen("Unexpected container " + container); } } diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 7f605f1980..1dea290fe5 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -131,6 +131,39 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { argumentDeletions[0].argumentName == "arg1" } + def "applied directive argument deleted interface"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on INTERFACE + + type Query implements I{ + foo: String + } + interface I @d(arg1: "foo") { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg1:String) on INTERFACE + + type Query implements I { + foo: String + } + interface I @d{ + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def argumentDeletions = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = argumentDeletions[0].locationDetail as AppliedDirectiveInterfaceLocation + location.name == "I" + argumentDeletions[0].argumentName == "arg1" + } + + def "applied directive added input object field "() { given: def oldSdl = ''' @@ -323,6 +356,32 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { argumentDeletions[0].argumentName == "arg1" } + def "applied directive argument deleted object"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on OBJECT + + type Query @d(arg1: "foo"){ + foo: String + } + ''' + def newSdl = ''' + directive @d(arg1: String) on OBJECT + + type Query @d { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def argumentDeletions = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = argumentDeletions[0].locationDetail as AppliedDirectiveObjectLocation + location.name == "Query" + argumentDeletions[0].argumentName == "arg1" + } + def "applied directive added input object"() { given: def oldSdl = ''' @@ -372,7 +431,7 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.objectDifferences["Query"] instanceof ObjectModification - (changes.objectDifferences as ObjectModification).details.size() == 1 + (changes.objectDifferences["Query"] as ObjectModification).details.size() == 1 def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveAddition) (appliedDirective[0].locationDetail as AppliedDirectiveObjectLocation).name == "Query" appliedDirective[0].name == "d" From 50ca9ddbbcf237592043ea587a60495d00688b1a Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 11:31:56 +1000 Subject: [PATCH 198/393] tests --- .../schema/diffing/ana/SchemaDifference.java | 10 +- ...rationAnalyzerAppliedDirectivesTest.groovy | 418 +++++++++++++++++- 2 files changed, 425 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java index 0436ac9e5f..177d65de72 100644 --- a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -1651,7 +1651,13 @@ public String getArgumentName() { } - class AppliedDirectiveArgumentValueModification implements ObjectModificationDetail { + class AppliedDirectiveArgumentValueModification implements ObjectModificationDetail, + InterfaceModificationDetail, + InputObjectModificationDetail, + EnumModificationDetail, + UnionModificationDetail, + ScalarModificationDetail, + DirectiveModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String argumentName; private final String oldValue; @@ -1681,7 +1687,7 @@ public String getNewValue() { } } - class AppliedDirectiveArgumentRename implements ObjectModificationDetail { + class AppliedDirectiveArgumentRename implements ObjectModificationDetail, InterfaceModificationDetail { private final AppliedDirectiveLocationDetail locationDetail; private final String oldName; private final String newName; diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index 1dea290fe5..c637cad1f0 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -66,6 +66,422 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { argumentAddition[0].argumentName == "arg1" } + def "applied directive argument value changed object"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on OBJECT + + type Query @d(arg1:"foo") { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg1: String) on OBJECT + + type Query @d(arg1: "bar") { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def detail = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveObjectLocation + location.name == "Query" + location.directiveName == "d" + detail[0].argumentName == "arg1" + detail[0].oldValue == "foo" + detail[0].oldValue == "bar" + } + + def "applied directive argument value changed object field"() { + given: + def oldSdl = ''' + directive @d(arg:String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "foo") + } + ''' + def newSdl = ''' + directive @d(arg: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "bar") + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def detail = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveObjectFieldLocation + location.objectName == "Query" + location.fieldName == "foo" + location.directiveName == "d" + detail[0].argumentName == "arg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + def "applied directive argument value changed object field argument"() { + given: + def oldSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d(directiveArg: "foo")) : String + } + ''' + def newSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d(directiveArg: "bar")) : String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def detail = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentValueModification) + def locationDetail = detail[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation + locationDetail.objectName == "Query" + locationDetail.fieldName == "foo" + locationDetail.argumentName == "arg" + locationDetail.directiveName == "d" + detail[0].argumentName == "directiveArg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + + def "applied directive argument value changed interface"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on INTERFACE + + type Query implements I{ + foo: String + } + interface I @d(arg1: "foo") { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg1:String) on INTERFACE + + type Query implements I{ + foo: String + } + interface I @d(arg1: "bar") { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def detail = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveInterfaceLocation + location.name == "I" + location.directiveName == "d" + detail[0].argumentName == "arg1" + detail[0].oldValue == "foo" + detail[0].oldValue == "bar" + } + + + def "applied directive argument value changed interface field"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query implements I{ + foo: String + } + interface I { + foo: String @d(arg1: "foo") + } + ''' + def newSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query implements I{ + foo: String + } + interface I { + foo: String @d(arg1: "bar") + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def change = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = change[0].locationDetail as AppliedDirectiveInterfaceFieldLocation + location.interfaceName == "I" + location.fieldName == "foo" + location.directiveName == "d" + change[0].argumentName == "arg1" + change[0].oldValue == "foo" + change[0].newValue == "bar" + } + + def "applied directive argument value changed interface field argument"() { + given: + def oldSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d(directiveArg: "foo") ): String + } + ''' + def newSdl = ''' + directive @d(directiveArg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d(directiveArg: "bar") ): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def detail = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation + location.interfaceName == "I" + location.fieldName == "foo" + location.argumentName == "arg" + location.directiveName == "d" + detail[0].argumentName == "directiveArg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + def "applied directive argument value changed input object"() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d(arg: "foo"){ + a: String + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d(arg: "bar") { + a: String + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def detail = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveInputObjectLocation + location.name == "I" + location.directiveName == "d" + detail[0].argumentName == "arg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + + } + + + def "applied directive argument value changed input object field "() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d(arg: "foo") + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d(arg: "bar") + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def detail = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveInputObjectFieldLocation + location.inputObjectName == "I" + location.fieldName == "a" + location.directiveName == "d" + detail[0].argumentName == "arg" + detail[0].oldValue == "foo" + detail[0].oldValue == "bar" + } + + def "applied directive argument value changed enum"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM + enum E @d(arg:"foo") { A, B } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM + enum E @d(arg: "bar") { A, B } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def detail = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveEnumLocation + location.name == "E" + location.directiveName == "d" + detail[0].argumentName == "arg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + def "applied directive argument value changed enum value"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d(arg: "foo") } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d(arg: "bar") } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def detail = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveEnumValueLocation + location.enumName == "E" + location.valueName == "B" + location.directiveName == "d" + detail[0].argumentName == "arg " + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + def "applied directive argument value changed union"() { + given: + def oldSdl = ''' + directive @d(arg: String) on UNION + type Query { + foo: U + } + union U @d(arg: "foo") = A | B + type A { a: String } + type B { b: String } + ''' + def newSdl = ''' + directive @d(arg: String) on UNION + type Query { + foo: U + } + union U @d(arg: "bar") = A | B + type A { a: String } + type B { b: String } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionModification + def detail = (changes.unionDifferences["U"] as UnionModification).getDetails(AppliedDirectiveArgumentValueModification) + (detail[0].locationDetail as AppliedDirectiveUnionLocation).name == "U" + detail[0].argumentName == "arg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + def "applied directive argument value changed scalar"() { + given: + def oldSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d(arg: "foo") + type Query { + foo: DateTime + } + ''' + def newSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d(arg: "bar") + type Query { + foo: DateTime + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["DateTime"] instanceof ScalarModification + def detail = (changes.scalarDifferences["DateTime"] as ScalarModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveScalarLocation + location.name == "DateTime" + location.directiveName == "d" + detail[0].argumentName == "arg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + def "applied directive argument value changed directive argument"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on ARGUMENT_DEFINITION + directive @d2(arg:String @d(arg1:"foo")) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg1:String) on ARGUMENT_DEFINITION + directive @d2(arg:String @d(arg1:"bar")) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d2"] instanceof DirectiveModification + (changes.directiveDifferences["d2"] as DirectiveModification).details.size() == 1 + def detail = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation + location.directiveName == "d2" + location.argumentName == "arg" + location.directiveName == "d" + detail[0].argumentName == "arg" + detail[0].oldValue == "foo" + detail[0].newValue == "bar" + } + + def "applied directive argument added interface"() { given: def oldSdl = ''' @@ -910,7 +1326,7 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { appliedDirective[0].name == "d" } - def "applied directive deleted argument directive"() { + def "applied directive deleted argument directive argument"() { given: def oldSdl = ''' directive @d(arg1:String) on ARGUMENT_DEFINITION From 34a2270dbd57cdb71602bf3dc1f1189822f4bcee Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 12:07:03 +1000 Subject: [PATCH 199/393] tests --- .../diffing/ana/EditOperationAnalyzer.java | 143 +++++++++++++++++- .../schema/diffing/ana/SchemaDifference.java | 11 +- ...rationAnalyzerAppliedDirectivesTest.groovy | 78 +++++----- 3 files changed, 189 insertions(+), 43 deletions(-) diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java index 1edd0840c4..d1da9830d1 100644 --- a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -390,7 +390,7 @@ private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { if (isAppliedDirectiveDeleted(fieldOrDirective, appliedDirective.getName())) { return; } - AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(appliedDirective.getName(), argument.getName()); + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName(), appliedDirective.getName()); getDirectiveModification(directive.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); } } else if (container.isOfType(SchemaGraph.FIELD)) { @@ -570,7 +570,7 @@ private void appliedDirectiveArgumentAdded(EditOperation editOperation) { if (isAppliedDirectiveAdded(directive, appliedDirective.getName())) { return; } - AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(appliedDirective.getName(), argument.getName()); + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName(), appliedDirective.getName()); getDirectiveModification(directive.getName()).getDetails().add(new AppliedDirectiveArgumentAddition(location, addedArgument.getName())); } } else if (container.isOfType(SchemaGraph.FIELD)) { @@ -728,7 +728,142 @@ private void appliedDirectiveArgumentChanged(EditOperation editOperation) { AppliedDirectiveArgumentRename argumentRename = new AppliedDirectiveArgumentRename(location, oldArgumentName, newArgumentName); getObjectModification(object.getName()).getDetails().add(argumentRename); } + } else if (interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = interfaceOrObjective; + AppliedDirectiveInterfaceFieldLocation location = new AppliedDirectiveInterfaceFieldLocation(interfaze.getName(), field.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getInterfaceModification(interfaze.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + } + } else if (container.isOfType(SchemaGraph.ARGUMENT)) { + Vertex argument = container; + Vertex fieldOrDirective = newSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex fieldsContainer = newSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainer.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainer; + AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName(), appliedDirective.getName()); + + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getObjectModification(object.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + } else if (fieldsContainer.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = fieldsContainer; + AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName(), appliedDirective.getName()); + + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getInterfaceModification(interfaze.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + } + } else if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { + Vertex directive = fieldOrDirective; + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getDirectiveModification(directive.getName()).getDetails().add(argumentValueModification); + } + + } + } else if (container.isOfType(SchemaGraph.INPUT_FIELD)) { + Vertex inputField = container; + Vertex inputObject = newSchemaGraph.getInputObjectForInputField(inputField); + AppliedDirectiveInputObjectFieldLocation location = new AppliedDirectiveInputObjectFieldLocation(inputObject.getName(), inputField.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getInputObjectModification(inputObject.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { +// AppliedDirectiveArgumentRename argumentRename = new AppliedDirectiveArgumentRename(location, oldArgumentName, newArgumentName); +// getInputObjectModification(inputObject.getName()).getDetails().add(argumentRename); + } + } else if (container.isOfType(SchemaGraph.OBJECT)) { + Vertex object = container; + AppliedDirectiveObjectLocation location = new AppliedDirectiveObjectLocation(object.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getObjectModification(object.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + } + } else if (container.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = container; + AppliedDirectiveInterfaceLocation location = new AppliedDirectiveInterfaceLocation(interfaze.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getInterfaceModification(interfaze.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + Vertex inputObject = container; + AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getInputObjectModification(inputObject.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + + } else if (container.isOfType(SchemaGraph.ENUM)) { + Vertex enumVertex = container; + AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getEnumModification(enumVertex.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + } + } else if (container.isOfType(SchemaGraph.ENUM_VALUE)) { + Vertex enumValue = container; + Vertex enumVertex = newSchemaGraph.getEnumForEnumValue(enumValue); + AppliedDirectiveEnumValueLocation location = new AppliedDirectiveEnumValueLocation(enumVertex.getName(), enumValue.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getEnumModification(enumVertex.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + + } else if (container.isOfType(SchemaGraph.UNION)) { + Vertex union = container; + AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getUnionModification(union.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + } else if (container.isOfType(SchemaGraph.SCALAR)) { + Vertex scalar = container; + AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName(), appliedDirective.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getScalarModification(scalar.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + + } + } else { + assertShouldNeverHappen("Unexpected applied argument container " + container); } } @@ -896,7 +1031,7 @@ private void appliedDirectiveDeletedFromArgument(Vertex appliedDirective, Vertex if (isArgumentDeletedFromExistingDirective(directive.getName(), argument.getName())) { return; } - AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName()); + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName(), appliedDirective.getName()); AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); getDirectiveModification(directive.getName()).getDetails().add(appliedDirectiveDeletion); } @@ -947,7 +1082,7 @@ private void appliedDirectiveAddedToArgument(Vertex appliedDirective, Vertex con if (isArgumentNewForExistingDirective(directive.getName(), argument.getName())) { return; } - AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName()); + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName(), appliedDirective.getName()); AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); getDirectiveModification(directive.getName()).getDetails().add(appliedDirectiveAddition); } diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java index 177d65de72..d6a2c8e494 100644 --- a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -1389,12 +1389,15 @@ public String getDirectiveName() { } class AppliedDirectiveDirectiveArgumentLocation implements AppliedDirectiveLocationDetail { + // this is the applied directive name private final String directiveName; + private final String directiveDefinitionName; private final String argumentName; - public AppliedDirectiveDirectiveArgumentLocation(String directiveName, String argumentName) { - this.directiveName = directiveName; + public AppliedDirectiveDirectiveArgumentLocation(String directiveDefinitionName, String argumentName, String directiveName) { + this.directiveDefinitionName = directiveDefinitionName; this.argumentName = argumentName; + this.directiveName = directiveName; } public String getDirectiveName() { @@ -1404,6 +1407,10 @@ public String getDirectiveName() { public String getArgumentName() { return argumentName; } + + public String getDirectiveDefinitionName() { + return directiveDefinitionName; + } } class AppliedDirectiveInterfaceFieldArgumentLocation implements AppliedDirectiveLocationDetail { diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy index c637cad1f0..2fc9336bcc 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -91,8 +91,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.name == "Query" location.directiveName == "d" detail[0].argumentName == "arg1" - detail[0].oldValue == "foo" - detail[0].oldValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed object field"() { @@ -121,8 +121,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.fieldName == "foo" location.directiveName == "d" detail[0].argumentName == "arg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed object field argument"() { @@ -150,8 +150,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { locationDetail.argumentName == "arg" locationDetail.directiveName == "d" detail[0].argumentName == "directiveArg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } @@ -186,8 +186,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.name == "I" location.directiveName == "d" detail[0].argumentName == "arg1" - detail[0].oldValue == "foo" - detail[0].oldValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } @@ -217,14 +217,14 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def changes = calcDiff(oldSdl, newSdl) then: changes.interfaceDifferences["I"] instanceof InterfaceModification - def change = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentValueModification) - def location = change[0].locationDetail as AppliedDirectiveInterfaceFieldLocation + def detail = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentValueModification) + def location = detail[0].locationDetail as AppliedDirectiveInterfaceFieldLocation location.interfaceName == "I" location.fieldName == "foo" location.directiveName == "d" - change[0].argumentName == "arg1" - change[0].oldValue == "foo" - change[0].newValue == "bar" + detail[0].argumentName == "arg1" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed interface field argument"() { @@ -258,8 +258,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.argumentName == "arg" location.directiveName == "d" detail[0].argumentName == "directiveArg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed input object"() { @@ -291,8 +291,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.name == "I" location.directiveName == "d" detail[0].argumentName == "arg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } @@ -327,8 +327,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.fieldName == "a" location.directiveName == "d" detail[0].argumentName == "arg" - detail[0].oldValue == "foo" - detail[0].oldValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed enum"() { @@ -356,8 +356,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.name == "E" location.directiveName == "d" detail[0].argumentName == "arg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed enum value"() { @@ -385,9 +385,9 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.enumName == "E" location.valueName == "B" location.directiveName == "d" - detail[0].argumentName == "arg " - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].argumentName == "arg" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed union"() { @@ -417,8 +417,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { def detail = (changes.unionDifferences["U"] as UnionModification).getDetails(AppliedDirectiveArgumentValueModification) (detail[0].locationDetail as AppliedDirectiveUnionLocation).name == "U" detail[0].argumentName == "arg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed scalar"() { @@ -446,8 +446,8 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { location.name == "DateTime" location.directiveName == "d" detail[0].argumentName == "arg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } def "applied directive argument value changed directive argument"() { @@ -473,12 +473,12 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { (changes.directiveDifferences["d2"] as DirectiveModification).details.size() == 1 def detail = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveArgumentValueModification) def location = detail[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation - location.directiveName == "d2" - location.argumentName == "arg" + location.directiveDefinitionName == "d2" location.directiveName == "d" - detail[0].argumentName == "arg" - detail[0].oldValue == "foo" - detail[0].newValue == "bar" + location.argumentName == "arg" + detail[0].argumentName == "arg1" + detail[0].oldValue == '"foo"' + detail[0].newValue == '"bar"' } @@ -1239,8 +1239,10 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { then: changes.directiveDifferences["d2"] instanceof DirectiveModification def appliedDirective = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveAddition) - (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).directiveName == "d2" - (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).argumentName == "arg" + def location = appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation + location.directiveDefinitionName == "d2" + location.argumentName == "arg" + location.directiveName == "d" appliedDirective[0].name == "d" } @@ -1349,8 +1351,10 @@ class EditOperationAnalyzerAppliedDirectivesTest extends Specification { // whole applied directive is deleted, so we don't count the applied argument deletion (changes.directiveDifferences["d2"] as DirectiveModification).details.size() == 1 def appliedDirective = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveDeletion) - (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).directiveName == "d2" - (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).argumentName == "arg" + def location = appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation + location.directiveDefinitionName == "d2" + location.argumentName == "arg" + location.directiveName == "d" appliedDirective[0].name == "d" } From 0c0fa2302685229e7ab58fe9d298155e971cac6e Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 12:14:45 +1000 Subject: [PATCH 200/393] fix test --- .../schema/diffing/ana/EditOperationAnalyzerTest.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerTest.groovy index 2ab7bbd7b2..8661293408 100644 --- a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerTest.groovy +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerTest.groovy @@ -1859,8 +1859,9 @@ class EditOperationAnalyzerTest extends Specification { directiveDeletion[0].locationDetail instanceof AppliedDirectiveDirectiveArgumentLocation def location = directiveDeletion[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation - location.directiveName == "d" + location.directiveDefinitionName == "d" location.argumentName == "message" + location.directiveName == "a" } def "field output type changed and applied directive removed"() { From cf2219c5bbd6776f0e13dbd9748d1c5b94955fd0 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 16 Feb 2024 12:18:50 +1000 Subject: [PATCH 201/393] formatting --- .../java/graphql/execution/Execution.java | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 6c3de7e436..3343b254d1 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -86,30 +86,30 @@ public CompletableFuture execute(Document document, GraphQLSche } ExecutionContext executionContext = newExecutionContextBuilder() - .instrumentation(instrumentation) - .instrumentationState(instrumentationState) - .executionId(executionId) - .graphQLSchema(graphQLSchema) - .queryStrategy(queryStrategy) - .mutationStrategy(mutationStrategy) - .subscriptionStrategy(subscriptionStrategy) - .context(executionInput.getContext()) - .graphQLContext(executionInput.getGraphQLContext()) - .localContext(executionInput.getLocalContext()) - .root(executionInput.getRoot()) - .fragmentsByName(fragmentsByName) - .coercedVariables(coercedVariables) - .document(document) - .operationDefinition(operationDefinition) - .dataLoaderRegistry(executionInput.getDataLoaderRegistry()) - .locale(executionInput.getLocale()) - .valueUnboxer(valueUnboxer) - .executionInput(executionInput) - .build(); + .instrumentation(instrumentation) + .instrumentationState(instrumentationState) + .executionId(executionId) + .graphQLSchema(graphQLSchema) + .queryStrategy(queryStrategy) + .mutationStrategy(mutationStrategy) + .subscriptionStrategy(subscriptionStrategy) + .context(executionInput.getContext()) + .graphQLContext(executionInput.getGraphQLContext()) + .localContext(executionInput.getLocalContext()) + .root(executionInput.getRoot()) + .fragmentsByName(fragmentsByName) + .coercedVariables(coercedVariables) + .document(document) + .operationDefinition(operationDefinition) + .dataLoaderRegistry(executionInput.getDataLoaderRegistry()) + .locale(executionInput.getLocale()) + .valueUnboxer(valueUnboxer) + .executionInput(executionInput) + .build(); InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters( - executionInput, graphQLSchema, instrumentationState + executionInput, graphQLSchema, instrumentationState ); executionContext = instrumentation.instrumentExecutionContext(executionContext, parameters, instrumentationState); return executeOperation(executionContext, executionInput.getRoot(), executionContext.getOperationDefinition()); @@ -142,12 +142,12 @@ private CompletableFuture executeOperation(ExecutionContext exe } FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() - .schema(executionContext.getGraphQLSchema()) - .objectType(operationRootType) - .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getCoercedVariables().toMap()) - .graphQLContext(graphQLContext) - .build(); + .schema(executionContext.getGraphQLSchema()) + .objectType(operationRootType) + .fragments(executionContext.getFragmentsByName()) + .variables(executionContext.getCoercedVariables().toMap()) + .graphQLContext(graphQLContext) + .build(); MergedSelectionSet fields = fieldCollector.collectFields( collectorParameters, @@ -162,13 +162,13 @@ private CompletableFuture executeOperation(ExecutionContext exe NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo); ExecutionStrategyParameters parameters = newParameters() - .executionStepInfo(executionStepInfo) - .source(root) - .localContext(executionContext.getLocalContext()) - .fields(fields) - .nonNullFieldValidator(nonNullableFieldValidator) - .path(path) - .build(); + .executionStepInfo(executionStepInfo) + .source(root) + .localContext(executionContext.getLocalContext()) + .fields(fields) + .nonNullFieldValidator(nonNullableFieldValidator) + .path(path) + .build(); CompletableFuture result; From 6c75035851c4c4de0c07b8a194cc0e11dd6e5a7b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 16 Feb 2024 15:16:09 +1100 Subject: [PATCH 202/393] Directive definitions now have a predicate and fixed up tests --- .../graphql/schema/idl/SchemaPrinter.java | 72 +++++++++++++++---- .../schema/idl/SchemaPrinterTest.groovy | 47 +++++++++++- .../groovy/graphql/util/AnonymizerTest.groovy | 4 +- 3 files changed, 107 insertions(+), 16 deletions(-) diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index 891eec5014..9ac97f6a2e 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -98,6 +98,8 @@ public static class Options { private final boolean descriptionsAsHashComments; + private final Predicate includeDirectiveDefinition; + private final Predicate includeDirective; private final Predicate includeSchemaElement; @@ -110,6 +112,7 @@ private Options(boolean includeIntrospectionTypes, boolean includeScalars, boolean includeSchemaDefinition, boolean includeDirectiveDefinitions, + Predicate includeDirectiveDefinition, boolean useAstDefinitions, boolean descriptionsAsHashComments, Predicate includeDirective, @@ -120,6 +123,7 @@ private Options(boolean includeIntrospectionTypes, this.includeScalars = includeScalars; this.includeSchemaDefinition = includeSchemaDefinition; this.includeDirectiveDefinitions = includeDirectiveDefinitions; + this.includeDirectiveDefinition = includeDirectiveDefinition; this.includeDirective = includeDirective; this.useAstDefinitions = useAstDefinitions; this.descriptionsAsHashComments = descriptionsAsHashComments; @@ -144,6 +148,10 @@ public boolean isIncludeDirectiveDefinitions() { return includeDirectiveDefinitions; } + public Predicate getIncludeDirectiveDefinition() { + return includeDirectiveDefinition; + } + public Predicate getIncludeDirective() { return includeDirective; } @@ -164,14 +172,16 @@ public boolean isUseAstDefinitions() { return useAstDefinitions; } - public boolean isIncludeAstDefinitionComments() { return includeAstDefinitionComments; } + public boolean isIncludeAstDefinitionComments() { + return includeAstDefinitionComments; + } public static Options defaultOptions() { return new Options(false, true, false, true, - false, + directive -> true, false, false, directive -> true, element -> true, @@ -191,7 +201,7 @@ public Options includeIntrospectionTypes(boolean flag) { this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, - this.useAstDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, @@ -211,7 +221,7 @@ public Options includeScalarTypes(boolean flag) { flag, this.includeSchemaDefinition, this.includeDirectiveDefinitions, - this.useAstDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, @@ -234,6 +244,7 @@ public Options includeSchemaDefinition(boolean flag) { this.includeScalars, flag, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, this.includeDirective, @@ -259,6 +270,29 @@ public Options includeDirectiveDefinitions(boolean flag) { this.includeScalars, this.includeSchemaDefinition, flag, + directive -> flag, + this.useAstDefinitions, + this.descriptionsAsHashComments, + this.includeDirective, + this.includeSchemaElement, + this.comparatorRegistry, + this.includeAstDefinitionComments); + } + + + /** + * This is a Predicate that decides whether a directive definition is printed. + * + * @param includeDirectiveDefinition the predicate to decide of a directive defintion is printed + * + * @return new instance of options + */ + public Options includeDirectiveDefinition(Predicate includeDirectiveDefinition) { + return new Options(this.includeIntrospectionTypes, + this.includeScalars, + this.includeSchemaDefinition, + this.includeDirectiveDefinitions, + includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, this.includeDirective, @@ -280,6 +314,7 @@ public Options includeDirectives(boolean flag) { this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, directive -> flag, @@ -300,6 +335,7 @@ public Options includeDirectives(Predicate includeDirective) { this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, includeDirective, @@ -308,6 +344,7 @@ public Options includeDirectives(Predicate includeDirective) { this.includeAstDefinitionComments); } + /** * This is a general purpose Predicate that decides whether a schema element is printed ever. * @@ -321,6 +358,7 @@ public Options includeSchemaElement(Predicate includeSchem this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, this.includeDirective, @@ -342,6 +380,7 @@ public Options useAstDefinitions(boolean flag) { this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, flag, this.descriptionsAsHashComments, this.includeDirective, @@ -365,6 +404,7 @@ public Options descriptionsAsHashComments(boolean flag) { this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, flag, this.includeDirective, @@ -387,6 +427,7 @@ public Options setComparators(GraphqlTypeComparatorRegistry comparatorRegistry) this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, this.includeDirective, @@ -409,6 +450,7 @@ public Options includeAstDefinitionComments(boolean flag) { this.includeScalars, this.includeSchemaDefinition, this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, this.useAstDefinitions, this.descriptionsAsHashComments, this.includeDirective, @@ -638,7 +680,9 @@ private SchemaElementPrinter unionPrinter() { private SchemaElementPrinter directivePrinter() { return (out, directive, visibility) -> { - if (options.isIncludeDirectiveDefinitions()) { + boolean isOnEver = options.isIncludeDirectiveDefinitions(); + boolean specificTest = options.getIncludeDirectiveDefinition().test(directive.getName()); + if (isOnEver && specificTest) { String s = directiveDefinition(directive); out.format("%s", s); out.print("\n\n"); @@ -964,14 +1008,14 @@ private boolean hasDeprecatedDirective(List directives) private List addDeprecatedDirectiveIfNeeded(GraphQLDirectiveContainer directiveContainer) { List directives = DirectivesUtil.toAppliedDirectives(directiveContainer); - if (!hasDeprecatedDirective(directives) && isDeprecatedDirectiveAllowed()) { + if (!hasDeprecatedDirective(directives) && isDeprecatedDirectiveAllowed()) { directives = new ArrayList<>(directives); - String reason = getDeprecationReason(directiveContainer); - GraphQLAppliedDirectiveArgument arg = GraphQLAppliedDirectiveArgument.newArgument() - .name("reason") - .valueProgrammatic(reason) - .type(GraphQLString) - .build(); + String reason = getDeprecationReason(directiveContainer); + GraphQLAppliedDirectiveArgument arg = GraphQLAppliedDirectiveArgument.newArgument() + .name("reason") + .valueProgrammatic(reason) + .type(GraphQLString) + .build(); GraphQLAppliedDirective directive = GraphQLAppliedDirective.newDirective() .name("deprecated") .argument(arg) @@ -1108,7 +1152,7 @@ private void printComments(PrintWriter out, Object graphQLType, String prefix) { if (options.isIncludeAstDefinitionComments()) { String commentsText = getAstDefinitionComments(graphQLType); if (!isNullOrEmpty(commentsText)) { - List lines = Arrays.asList(commentsText.split("\n") ); + List lines = Arrays.asList(commentsText.split("\n")); if (!lines.isEmpty()) { printMultiLineHashDescription(out, prefix, lines); } @@ -1183,7 +1227,7 @@ private String getAstDefinitionComments(Object commentHolder) { } private String comments(List comments) { - if ( comments == null || comments.isEmpty() ) { + if (comments == null || comments.isEmpty()) { return null; } String s = comments.stream().map(c -> c.getContent()).collect(joining("\n", "", "\n")); diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy index 13e0319233..1cff3fffec 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy @@ -2710,7 +2710,7 @@ input Gun { .build() when: - def printOptions = defaultOptions().includeDirectives({d -> true}) + def printOptions = defaultOptions().includeDirectiveDefinitions(false).includeDirectives({ d -> true }) def result = "\n" + new SchemaPrinter(printOptions).print(schema) println(result) @@ -2734,4 +2734,49 @@ input Input { } """ } + + def "can use predicate for directive definitions"() { + + def schema = TestUtil.schema(""" + type Query { + field: String @deprecated + } + """) + + + def options = defaultOptions() + .includeDirectiveDefinitions(true) + .includeDirectiveDefinition({ it != "skip" }) + def result = new SchemaPrinter(options).print(schema) + + expect: "has no skip definition" + + result == """"Marks the field, argument, input field or enum value as deprecated" +directive @deprecated( + "The reason for the deprecation" + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + +"Directs the executor to include this field or fragment only when the `if` argument is true" +directive @include( + "Included when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Indicates an Input Object is a OneOf Input Object." +directive @oneOf on INPUT_OBJECT + +"Exposes a URL that specifies the behaviour of this scalar." +directive @specifiedBy( + "The URL that specifies the behaviour of this scalar." + url: String! + ) on SCALAR + +type Query { + field: String @deprecated(reason : "No longer supported") } +""" + } +} + + diff --git a/src/test/groovy/graphql/util/AnonymizerTest.groovy b/src/test/groovy/graphql/util/AnonymizerTest.groovy index d1bf02e20a..439132eda3 100644 --- a/src/test/groovy/graphql/util/AnonymizerTest.groovy +++ b/src/test/groovy/graphql/util/AnonymizerTest.groovy @@ -718,7 +718,7 @@ type Object1 { when: def result = Anonymizer.anonymizeSchema(schema) def newSchema = new SchemaPrinter(SchemaPrinter.Options.defaultOptions() - .includeDirectives({!DirectiveInfo.isGraphqlSpecifiedDirective(it)})) + .includeDirectives({!DirectiveInfo.isGraphqlSpecifiedDirective(it) || it == "deprecated"})) .print(result) then: @@ -729,6 +729,8 @@ type Object1 { directive @Directive1(argument1: String! = "stringValue4") repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + directive @deprecated(reason: String) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + interface Interface1 @Directive1(argument1 : "stringValue12") { field2: String field3: Enum1 From 17a401a776f271eba340d9efc44502173f710f3c Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 11:28:25 +1000 Subject: [PATCH 203/393] naming --- .../java/graphql/execution/AsyncExecutionStrategy.java | 4 ++-- .../graphql/execution/DataLoaderDispatchStrategy.java | 8 ++++---- src/main/java/graphql/execution/ExecutionStrategy.java | 4 ++-- .../dataloader/PerLevelDataLoaderDispatchStrategy.java | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index aeb5c68b6e..01aedcf8d6 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -64,14 +64,14 @@ public CompletableFuture execute(ExecutionContext executionCont for (FieldValueInfo completeValueInfo : completeValueInfos) { fieldValuesFutures.add(completeValueInfo.getFieldValueFuture()); } - executionContext.getDataLoaderDispatcherStrategy().executionStrategy_onFieldValuesInfo(completeValueInfos, parameters); + executionContext.getDataLoaderDispatcherStrategy().executionStrategyOnFieldValuesInfo(completeValueInfos, parameters); executionStrategyCtx.onFieldValuesInfo(completeValueInfos); fieldValuesFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. - executionContext.getDataLoaderDispatcherStrategy().executionStrategy_onFieldValuesException(ex, parameters); + executionContext.getDataLoaderDispatcherStrategy().executionStrategyOnFieldValuesException(ex, parameters); executionStrategyCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; diff --git a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java index 9a54ec3cbf..7cdc32d980 100644 --- a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java @@ -17,11 +17,11 @@ default void executionStrategy(ExecutionContext executionContext, ExecutionStrat } - default void executionStrategy_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { + default void executionStrategyOnFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { } - default void executionStrategy_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { + default void executionStrategyOnFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { } @@ -30,11 +30,11 @@ default void executeObject(ExecutionContext executionContext, ExecutionStrategyP } - default void executeObject_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { + default void executeObjectOnFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { } - default void executeObject_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { + default void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 8b87ed17ca..d53bd14cf9 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -221,14 +221,14 @@ protected CompletableFuture> executeObject(ExecutionContext for (FieldValueInfo completeValueInfo : completeValueInfos) { resultFutures.add(completeValueInfo.getFieldValueFuture()); } - executionContext.getDataLoaderDispatcherStrategy().executeObject_onFieldValuesInfo(completeValueInfos, parameters); + executionContext.getDataLoaderDispatcherStrategy().executeObjectOnFieldValuesInfo(completeValueInfos, parameters); resolveObjectCtx.onFieldValuesInfo(completeValueInfos); resultFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. - executionContext.getDataLoaderDispatcherStrategy().executeObject_onFieldValuesException(ex, parameters); + executionContext.getDataLoaderDispatcherStrategy().executeObjectOnFieldValuesException(ex, parameters); resolveObjectCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index 4e2b82623d..26a00fadf7 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -106,12 +106,12 @@ public void executionStrategy(ExecutionContext executionContext, ExecutionStrate } @Override - public void executionStrategy_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { + public void executionStrategyOnFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { int curLevel = parameters.getPath().getLevel() + 1; onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel, parameters); } - public void executionStrategy_onFieldValuesException(Throwable t, ExecutionStrategyParameters executionStrategyParameters) { + public void executionStrategyOnFieldValuesException(Throwable t, ExecutionStrategyParameters executionStrategyParameters) { int curLevel = executionStrategyParameters.getPath().getLevel() + 1; synchronized (callStack) { callStack.increaseHappenedOnFieldValueCalls(curLevel); @@ -126,14 +126,14 @@ public void executeObject(ExecutionContext executionContext, ExecutionStrategyPa } @Override - public void executeObject_onFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { + public void executeObjectOnFieldValuesInfo(List fieldValueInfoList, ExecutionStrategyParameters parameters) { int curLevel = parameters.getPath().getLevel() + 1; onFieldValuesInfoDispatchIfNeeded(fieldValueInfoList, curLevel, parameters); } @Override - public void executeObject_onFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { + public void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { int curLevel = parameters.getPath().getLevel() + 1; synchronized (callStack) { callStack.increaseHappenedOnFieldValueCalls(curLevel); From 94d5a28dbdb166194d5fc5673b0eede159256bf4 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 11:49:04 +1000 Subject: [PATCH 204/393] test cleanup --- src/main/java/graphql/GraphQL.java | 32 ++-------------- src/test/groovy/graphql/GraphQLTest.groovy | 38 ++----------------- .../ChainedInstrumentationStateTest.groovy | 1 - .../InstrumentationTest.groovy | 2 - 4 files changed, 6 insertions(+), 67 deletions(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index a83f9ea74d..af337139eb 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -16,7 +16,6 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.NoContextChainedInstrumentation; import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; @@ -210,7 +209,6 @@ public static class Builder { private ExecutionIdProvider idProvider = DEFAULT_EXECUTION_ID_PROVIDER; private Instrumentation instrumentation = null; // deliberate default here private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; - private boolean doNotAddDefaultInstrumentations = false; private boolean doNotAutomaticallyDispatchDataLoader = false; private ValueUnboxer valueUnboxer = ValueUnboxer.DEFAULT; @@ -266,20 +264,6 @@ public Builder executionIdProvider(ExecutionIdProvider executionIdProvider) { return this; } - /** - * As of GraphQL Java 22 there are no default instrumentations. Before that a DataLoaderDispatcherInstrumentation - * was added by default. - *

- * For GraphQL Java 22 calling this method is equal to calling {@link #doNotAutomaticallyDispatchDataLoader()} - * - * @return this builder - */ - public Builder doNotAddDefaultInstrumentations() { - this.doNotAddDefaultInstrumentations = true; - // legacy reasons: calling this method is equal to calling doNotAutomaticallyDispatchDataLoader - this.doNotAutomaticallyDispatchDataLoader = true; - return this; - } /** * Deactivates the automatic dispatching of DataLoaders. @@ -309,7 +293,9 @@ public GraphQL build() { this.subscriptionExecutionStrategy = new SubscriptionExecutionStrategy(this.defaultExceptionHandler); } - this.instrumentation = checkInstrumentationDefaultState(this.instrumentation, this.doNotAddDefaultInstrumentations); + if (instrumentation == null) { + this.instrumentation = SimplePerformantInstrumentation.INSTANCE; + } return new GraphQL(this); } } @@ -555,16 +541,4 @@ private CompletableFuture execute(ExecutionInput executionInput return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState); } - private static Instrumentation checkInstrumentationDefaultState(Instrumentation instrumentation, boolean doNotAddDefaultInstrumentations) { - if (doNotAddDefaultInstrumentations) { - return instrumentation == null ? SimplePerformantInstrumentation.INSTANCE : instrumentation; - } - if (instrumentation instanceof NoContextChainedInstrumentation) { - return instrumentation; - } - if (instrumentation == null) { - return SimplePerformantInstrumentation.INSTANCE; - } - return instrumentation; - } } diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index c3a5e2c7ff..ca1cfcdb85 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -971,12 +971,13 @@ class GraphQLTest extends Specification { then: result == [hello: 'world'] queryStrategy.executionId == goodbye - queryStrategy.instrumentation instanceof Instrumentation + queryStrategy.instrumentation instanceof SimplePerformantInstrumentation + newGraphQL.instrumentation == newInstrumentation // (queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(newInstrumentation) // !(queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(instrumentation) } - def "disabling data loader instrumentation leaves instrumentation as is"() { + def "provided instrumentation is unchanged"() { given: def queryStrategy = new CaptureStrategy() def instrumentation = new SimplePerformantInstrumentation() @@ -986,7 +987,6 @@ class GraphQLTest extends Specification { when: def graphql = builder - .doNotAddDefaultInstrumentations() .build() graphql.execute('{ hello }') @@ -994,38 +994,6 @@ class GraphQLTest extends Specification { queryStrategy.instrumentation == instrumentation } -// def "a single DataLoader instrumentation leaves instrumentation as is"() { -// given: -// def queryStrategy = new CaptureStrategy() -//// def instrumentation = new DataLoaderDispatcherInstrumentation() -// def builder = GraphQL.newGraphQL(simpleSchema()) -// .queryExecutionStrategy(queryStrategy) -// .instrumentation(instrumentation) -// -// when: -// def graphql = builder -// .build() -// graphql.execute('{ hello }') -// -// then: -// queryStrategy.instrumentation == instrumentation -// } - -// def "DataLoader instrumentation is the default instrumentation"() { -// given: -// def queryStrategy = new CaptureStrategy() -// def builder = GraphQL.newGraphQL(simpleSchema()) -// .queryExecutionStrategy(queryStrategy) -// -// when: -// def graphql = builder -// .build() -// graphql.execute('{ hello }') -// -// then: -// queryStrategy.instrumentation instanceof DataLoaderDispatcherInstrumentation -// } -// def "query with triple quoted multi line strings"() { given: def queryType = "Query" diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy index 78a1fe8755..9328b5a6c6 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy @@ -333,7 +333,6 @@ class ChainedInstrumentationStateTest extends Specification { def graphQL = GraphQL .newGraphQL(StarWarsSchema.starWarsSchema) .instrumentation(new ChainedInstrumentation([instrumentation1, instrumentation2])) - .doNotAddDefaultInstrumentations() // important, otherwise a chained one wil be used .build() when: diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index 9da4e4990c..a0247abbd5 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -375,7 +375,6 @@ class InstrumentationTest extends Specification { def graphQL = GraphQL .newGraphQL(StarWarsSchema.starWarsSchema) .instrumentation(instrumentation) - .doNotAddDefaultInstrumentations() // important, otherwise a chained one wil be used .build() when: @@ -447,7 +446,6 @@ class InstrumentationTest extends Specification { def graphQL = GraphQL .newGraphQL(StarWarsSchema.starWarsSchema) .instrumentation(instrumentation1) - .doNotAddDefaultInstrumentations() // important, otherwise a chained one wil be used .build() when: From ae4937cad66c9b49b13b4765696bf71125136ad9 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 11:59:31 +1000 Subject: [PATCH 205/393] PR feedback --- .../graphql/execution/ExecutionContext.java | 11 +- .../graphql/execution/ExecutionStrategy.java | 241 ++++++++++-------- 2 files changed, 140 insertions(+), 112 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 6758a0462c..71f6c1827b 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -60,7 +60,8 @@ public class ExecutionContext { private final ExecutionInput executionInput; private final Supplier queryTree; - private final AtomicReference dataLoaderDispatcherStrategy = new AtomicReference<>(DataLoaderDispatchStrategy.NO_OP); + // this is modified after creation so it needs to be volatile to ensure visibility across Threads + private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; ExecutionContext(ExecutionContextBuilder builder) { this.graphQLSchema = builder.graphQLSchema; @@ -250,7 +251,9 @@ public List getErrors() { return errors.get(); } - public ExecutionStrategy getQueryStrategy() { return queryStrategy; } + public ExecutionStrategy getQueryStrategy() { + return queryStrategy; + } public ExecutionStrategy getMutationStrategy() { return mutationStrategy; @@ -280,12 +283,12 @@ public Supplier getNormalizedQueryTree() { @Internal public void setDataLoaderDispatcherStrategy(DataLoaderDispatchStrategy dataLoaderDispatcherStrategy) { - this.dataLoaderDispatcherStrategy.set(dataLoaderDispatcherStrategy); + this.dataLoaderDispatcherStrategy = dataLoaderDispatcherStrategy; } @Internal public DataLoaderDispatchStrategy getDataLoaderDispatcherStrategy() { - return dataLoaderDispatcherStrategy.get(); + return dataLoaderDispatcherStrategy; } /** diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index d53bd14cf9..bded7c1a70 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -178,7 +178,9 @@ public static String mkNameForPath(List currentField) { * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object + * * @return a promise to an {@link ExecutionResult} + * * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ public abstract CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException; @@ -188,7 +190,9 @@ public static String mkNameForPath(List currentField) { * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object + * * @return a promise to a map of object field values + * * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ protected CompletableFuture> executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { @@ -197,7 +201,7 @@ protected CompletableFuture> executeObject(ExecutionContext InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); ExecuteObjectInstrumentationContext resolveObjectCtx = ExecuteObjectInstrumentationContext.nonNullCtx( - instrumentation.beginExecuteObject(instrumentationParameters, executionContext.getInstrumentationState()) + instrumentation.beginExecuteObject(instrumentationParameters, executionContext.getInstrumentationState()) ); List fieldNames = parameters.getFields().getKeys(); @@ -258,22 +262,22 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi MergedSelectionSet fields = parameters.getFields(); return Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) - .orElse(false) ? - new DeferredExecutionSupport.DeferredExecutionSupportImpl( - fields, - parameters, - executionContext, - this::resolveFieldWithInfo - ) : DeferredExecutionSupport.NOOP; + .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) + .orElse(false) ? + new DeferredExecutionSupport.DeferredExecutionSupportImpl( + fields, + parameters, + executionContext, + this::resolveFieldWithInfo + ) : DeferredExecutionSupport.NOOP; } @NotNull Async.CombinedBuilder getAsyncFieldValueInfo( - ExecutionContext executionContext, - ExecutionStrategyParameters parameters, - DeferredExecutionSupport deferredExecutionSupport + ExecutionContext executionContext, + ExecutionStrategyParameters parameters, + DeferredExecutionSupport deferredExecutionSupport ) { MergedSelectionSet fields = parameters.getFields(); @@ -281,14 +285,14 @@ Async.CombinedBuilder getAsyncFieldValueInfo( // Only non-deferred fields should be considered for calculating the expected size of futures. Async.CombinedBuilder futures = Async - .ofExpectedSize(fields.size() - deferredExecutionSupport.deferredFieldsCount()); + .ofExpectedSize(fields.size() - deferredExecutionSupport.deferredFieldsCount()); for (String fieldName : fields.getKeys()) { MergedField currentField = fields.getSubField(fieldName); ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); ExecutionStrategyParameters newParameters = parameters - .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); + .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); if (!deferredExecutionSupport.isDeferredField(currentField)) { CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); @@ -309,7 +313,9 @@ Async.CombinedBuilder getAsyncFieldValueInfo( * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object + * * @return a promise to an {@link Object} + * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ protected CompletableFuture resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -327,7 +333,9 @@ protected CompletableFuture resolveField(ExecutionContext executionConte * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object + * * @return a promise to a {@link FieldValueInfo} + * * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value */ protected CompletableFuture resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -336,12 +344,12 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationContext fieldCtx = nonNullCtx(instrumentation.beginFieldExecution( - new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() + new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() )); CompletableFuture fetchFieldFuture = fetchField(executionContext, parameters); CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> - completeField(executionContext, parameters, fetchedValue)); + completeField(executionContext, parameters, fetchedValue)); CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); @@ -359,7 +367,9 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object + * * @return a promise to a fetched object + * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ protected CompletableFuture fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -373,7 +383,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC Supplier dataFetchingEnvironment = FpKit.intraThreadMemoize(() -> { Supplier executionStepInfo = FpKit.intraThreadMemoize( - () -> createExecutionStepInfo(executionContext, parameters, fieldDef, parentType)); + () -> createExecutionStepInfo(executionContext, parameters, fieldDef, parentType)); Supplier> argumentValues = () -> executionStepInfo.get().getArguments(); @@ -382,24 +392,24 @@ protected CompletableFuture fetchField(ExecutionContext executionC // DataFetchingFieldSelectionSet and QueryDirectives is a supplier of sorts - eg a lazy pattern DataFetchingFieldSelectionSet fieldCollector = DataFetchingFieldSelectionSetImpl.newCollector(executionContext.getGraphQLSchema(), fieldDef.getType(), normalizedFieldSupplier); QueryDirectives queryDirectives = new QueryDirectivesImpl(field, - executionContext.getGraphQLSchema(), - executionContext.getCoercedVariables().toMap(), - executionContext.getGraphQLContext(), - executionContext.getLocale()); + executionContext.getGraphQLSchema(), + executionContext.getCoercedVariables().toMap(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); return newDataFetchingEnvironment(executionContext) - .source(parameters.getSource()) - .localContext(parameters.getLocalContext()) - .arguments(argumentValues) - .fieldDefinition(fieldDef) - .mergedField(parameters.getField()) - .fieldType(fieldDef.getType()) - .executionStepInfo(executionStepInfo) - .parentType(parentType) - .selectionSet(fieldCollector) - .queryDirectives(queryDirectives) - .build(); + .source(parameters.getSource()) + .localContext(parameters.getLocalContext()) + .arguments(argumentValues) + .fieldDefinition(fieldDef) + .mergedField(parameters.getField()) + .fieldType(fieldDef.getType()) + .executionStepInfo(executionStepInfo) + .parentType(parentType) + .selectionSet(fieldCollector) + .queryDirectives(queryDirectives) + .build(); }); DataFetcher dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); @@ -407,7 +417,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, dataFetcher instanceof TrivialDataFetcher); InstrumentationContext fetchCtx = nonNullCtx(instrumentation.beginFieldFetch(instrumentationFieldFetchParams, - executionContext.getInstrumentationState()) + executionContext.getInstrumentationState()) ); dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); @@ -416,17 +426,17 @@ protected CompletableFuture fetchField(ExecutionContext executionC executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); fetchCtx.onDispatched(fetchedValue); return fetchedValue - .handle((result, exception) -> { - fetchCtx.onCompleted(result, exception); - if (exception != null) { - return handleFetchingException(dataFetchingEnvironment.get(), parameters, exception); - } else { - // we can simply return the fetched value CF and avoid a allocation - return fetchedValue; - } - }) - .thenCompose(Function.identity()) - .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); + .handle((result, exception) -> { + fetchCtx.onCompleted(result, exception); + if (exception != null) { + return handleFetchingException(dataFetchingEnvironment.get(), parameters, exception); + } else { + // we can simply return the fetched value CF and avoid a allocation + return fetchedValue; + } + }) + .thenCompose(Function.identity()) + .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); } private CompletableFuture invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { @@ -465,17 +475,17 @@ protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext execution localContext = parameters.getLocalContext(); } return FetchedValue.newFetchedValue() - .fetchedValue(executionContext.getValueUnboxer().unbox(dataFetcherResult.getData())) - .rawFetchedValue(dataFetcherResult.getData()) - .errors(dataFetcherResult.getErrors()) - .localContext(localContext) - .build(); + .fetchedValue(executionContext.getValueUnboxer().unbox(dataFetcherResult.getData())) + .rawFetchedValue(dataFetcherResult.getData()) + .errors(dataFetcherResult.getErrors()) + .localContext(localContext) + .build(); } else { return FetchedValue.newFetchedValue() - .fetchedValue(executionContext.getValueUnboxer().unbox(result)) - .rawFetchedValue(result) - .localContext(parameters.getLocalContext()) - .build(); + .fetchedValue(executionContext.getValueUnboxer().unbox(result)) + .rawFetchedValue(result) + .localContext(parameters.getLocalContext()) + .build(); } } @@ -490,28 +500,28 @@ private void addExtensionsIfPresent(ExecutionContext executionContext, DataFetch } protected CompletableFuture handleFetchingException( - DataFetchingEnvironment environment, - ExecutionStrategyParameters parameters, - Throwable e + DataFetchingEnvironment environment, + ExecutionStrategyParameters parameters, + Throwable e ) { DataFetcherExceptionHandlerParameters handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() - .dataFetchingEnvironment(environment) - .exception(e) - .build(); + .dataFetchingEnvironment(environment) + .exception(e) + .build(); parameters.getDeferredCallContext().onFetchingException( - parameters.getPath(), - parameters.getField().getSingleField().getSourceLocation(), - e + parameters.getPath(), + parameters.getField().getSingleField().getSourceLocation(), + e ); try { return asyncHandleException(dataFetcherExceptionHandler, handlerParameters); } catch (Exception handlerException) { handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() - .dataFetchingEnvironment(environment) - .exception(handlerException) - .build(); + .dataFetchingEnvironment(environment) + .exception(handlerException) + .build(); return asyncHandleException(new SimpleDataFetcherExceptionHandler(), handlerParameters); } } @@ -519,7 +529,7 @@ protected CompletableFuture handleFetchingException( private CompletableFuture asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) { //noinspection unchecked return handler.handleException(handlerParameters).thenApply( - handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build() + handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build() ); } @@ -535,7 +545,9 @@ private CompletableFuture asyncHandleException(DataFetcherExceptionHandle * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param fetchedValue the fetched raw value + * * @return a {@link FieldValueInfo} + * * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value */ protected FieldValueInfo completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) { @@ -547,16 +559,16 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, fetchedValue); InstrumentationContext ctxCompleteField = nonNullCtx(instrumentation.beginFieldCompletion( - instrumentationParams, executionContext.getInstrumentationState() + instrumentationParams, executionContext.getInstrumentationState() )); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo); ExecutionStrategyParameters newParameters = parameters.transform(builder -> - builder.executionStepInfo(executionStepInfo) - .source(fetchedValue.getFetchedValue()) - .localContext(fetchedValue.getLocalContext()) - .nonNullFieldValidator(nonNullableFieldValidator) + builder.executionStepInfo(executionStepInfo) + .source(fetchedValue.getFetchedValue()) + .localContext(fetchedValue.getLocalContext()) + .nonNullFieldValidator(nonNullableFieldValidator) ); FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters); @@ -578,7 +590,9 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object + * * @return a {@link FieldValueInfo} + * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected FieldValueInfo completeValue(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { @@ -627,7 +641,9 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra * Called to complete a null value. * * @param parameters contains the parameters holding the fields to be executed and source object + * * @return a {@link FieldValueInfo} + * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters parameters) { @@ -649,6 +665,7 @@ protected CompletableFuture completeValueForNull(ExecutionStrategyParame * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param result the result to complete, raw result + * * @return a {@link FieldValueInfo} */ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Object result) { @@ -671,6 +688,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param iterableValues the values to complete, can't be null + * * @return a {@link FieldValueInfo} */ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Iterable iterableValues) { @@ -682,7 +700,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationContext completeListCtx = nonNullCtx(instrumentation.beginFieldListCompletion( - instrumentationParams, executionContext.getInstrumentationState() + instrumentationParams, executionContext.getInstrumentationState() )); List fieldValueInfos = new ArrayList<>(size.orElse(1)); @@ -697,11 +715,11 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, FetchedValue value = unboxPossibleDataFetcherResult(executionContext, parameters, item); ExecutionStrategyParameters newParameters = parameters.transform(builder -> - builder.executionStepInfo(stepInfoForListElement) - .nonNullFieldValidator(nonNullableFieldValidator) - .localContext(value.getLocalContext()) - .path(indexedPath) - .source(value.getFetchedValue()) + builder.executionStepInfo(stepInfoForListElement) + .nonNullFieldValidator(nonNullableFieldValidator) + .localContext(value.getLocalContext()) + .path(indexedPath) + .source(value.getFetchedValue()) ); fieldValueInfos.add(completeValue(executionContext, newParameters)); index++; @@ -724,9 +742,9 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, }); return FieldValueInfo.newFieldValueInfo(LIST) - .fieldValue(overallResult) - .fieldValueInfos(fieldValueInfos) - .build(); + .fieldValue(overallResult) + .fieldValueInfos(fieldValueInfos) + .build(); } protected void handleValueException(CompletableFuture overallResult, Throwable e, ExecutionContext executionContext) { @@ -759,6 +777,7 @@ protected void handleValueException(CompletableFuture overallResult, Thro * @param parameters contains the parameters holding the fields to be executed and source object * @param scalarType the type of the scalar * @param result the result to be coerced + * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { @@ -784,6 +803,7 @@ protected CompletableFuture completeValueForScalar(ExecutionContext exec * @param parameters contains the parameters holding the fields to be executed and source object * @param enumType the type of the enum * @param result the result to be coerced + * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { @@ -808,35 +828,36 @@ protected CompletableFuture completeValueForEnum(ExecutionContext execut * @param parameters contains the parameters holding the fields to be executed and source object * @param resolvedObjectType the resolved object type * @param result the result to be coerced + * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture> completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); FieldCollectorParameters collectorParameters = newParameters() - .schema(executionContext.getGraphQLSchema()) - .objectType(resolvedObjectType) - .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getCoercedVariables().toMap()) - .graphQLContext(executionContext.getGraphQLContext()) - .build(); + .schema(executionContext.getGraphQLSchema()) + .objectType(resolvedObjectType) + .fragments(executionContext.getFragmentsByName()) + .variables(executionContext.getCoercedVariables().toMap()) + .graphQLContext(executionContext.getGraphQLContext()) + .build(); MergedSelectionSet subFields = fieldCollector.collectFields( - collectorParameters, - parameters.getField(), - Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) - .orElse(false) + collectorParameters, + parameters.getField(), + Optional.ofNullable(executionContext.getGraphQLContext()) + .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) + .orElse(false) ); ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull(resolvedObjectType); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, newExecutionStepInfo); ExecutionStrategyParameters newParameters = parameters.transform(builder -> - builder.executionStepInfo(newExecutionStepInfo) - .fields(subFields) - .nonNullFieldValidator(nonNullableFieldValidator) - .source(result) + builder.executionStepInfo(newExecutionStepInfo) + .fields(subFields) + .nonNullFieldValidator(nonNullableFieldValidator) + .source(result) ); // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. @@ -883,6 +904,7 @@ private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrate * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param field the field to find the definition of + * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Field field) { @@ -896,6 +918,7 @@ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, * @param schema the schema in play * @param parentType the parent type of the field * @param field the field to find the definition of + * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObjectType parentType, Field field) { @@ -913,6 +936,7 @@ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObject * * @param e this indicates that a null value was returned for a non null field, which needs to cause the parent field * to become null OR continue on as an exception + * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected void assertNonNullFieldPrecondition(NonNullableFieldWasNullException e) throws NonNullableFieldWasNullException { @@ -959,6 +983,7 @@ protected ExecutionResult handleNonNullException(ExecutionContext executionConte * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldDefinition the field definition to build type info for * @param fieldContainer the field container + * * @return a new type info */ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionContext, @@ -977,23 +1002,23 @@ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionCo List fieldArgs = field.getArguments(); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); Supplier> argValuesSupplier = () -> ValuesResolver.getArgumentValues(codeRegistry, - fieldArgDefs, - fieldArgs, - executionContext.getCoercedVariables(), - executionContext.getGraphQLContext(), - executionContext.getLocale()); + fieldArgDefs, + fieldArgs, + executionContext.getCoercedVariables(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); argumentValues = FpKit.intraThreadMemoize(argValuesSupplier); } return newExecutionStepInfo() - .type(fieldType) - .fieldDefinition(fieldDefinition) - .fieldContainer(fieldContainer) - .field(field) - .path(parameters.getPath()) - .parentInfo(parentStepInfo) - .arguments(argumentValues) - .build(); + .type(fieldType) + .fieldDefinition(fieldDefinition) + .fieldContainer(fieldContainer) + .field(field) + .path(parameters.getPath()) + .parentInfo(parentStepInfo) + .arguments(argumentValues) + .build(); } } From 5a220010093583aaa8bbc1698e036d37021c802c Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 12:07:43 +1000 Subject: [PATCH 206/393] PR feedback and cleanup --- .../graphql/execution/ExecutionStrategy.java | 2 +- src/test/groovy/example/http/HttpMain.java | 11 +--- src/test/groovy/graphql/GraphQLTest.groovy | 2 - ...groovy => DataLoaderDispatcherTest.groovy} | 50 +------------------ .../Issue1178DataLoaderDispatchTest.groovy | 2 - ...eCompaniesAndProductsDataLoaderTest.groovy | 1 - .../readme/DataLoaderBatchingExamples.java | 8 --- 7 files changed, 5 insertions(+), 71 deletions(-) rename src/test/groovy/graphql/execution/instrumentation/dataloader/{DataLoaderDispatcherInstrumentationTest.groovy => DataLoaderDispatcherTest.groovy} (79%) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index bded7c1a70..156f07cadc 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -420,8 +420,8 @@ protected CompletableFuture fetchField(ExecutionContext executionC executionContext.getInstrumentationState()) ); - dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); + dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); fetchCtx.onDispatched(fetchedValue); diff --git a/src/test/groovy/example/http/HttpMain.java b/src/test/groovy/example/http/HttpMain.java index 822352209a..b823b78060 100644 --- a/src/test/groovy/example/http/HttpMain.java +++ b/src/test/groovy/example/http/HttpMain.java @@ -4,6 +4,7 @@ import graphql.ExecutionResult; import graphql.GraphQL; import graphql.StarWarsData; +import graphql.execution.instrumentation.tracing.TracingInstrumentation; import graphql.schema.DataFetcher; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -134,18 +135,10 @@ private void handleStarWars(HttpServletRequest httpRequest, HttpServletResponse // you need a schema in order to execute queries GraphQLSchema schema = buildStarWarsSchema(); - // DataLoaderDispatcherInstrumentation dlInstrumentation = - // new DataLoaderDispatcherInstrumentation(newOptions().includeStatistics(true)); - - // Instrumentation instrumentation = new ChainedInstrumentation( - // asList(new TracingInstrumentation(), dlInstrumentation) - // ); - // finally you build a runtime graphql object and execute the query GraphQL graphQL = GraphQL .newGraphQL(schema) - // instrumentation is pluggable - // .instrumentation(instrumentation) + .instrumentation(new TracingInstrumentation()) .build(); ExecutionResult executionResult = graphQL.execute(executionInput); diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index ca1cfcdb85..d3683407a7 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -973,8 +973,6 @@ class GraphQLTest extends Specification { queryStrategy.executionId == goodbye queryStrategy.instrumentation instanceof SimplePerformantInstrumentation newGraphQL.instrumentation == newInstrumentation -// (queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(newInstrumentation) -// !(queryStrategy.instrumentation as ChainedInstrumentation).getInstrumentations().contains(instrumentation) } def "provided instrumentation is unchanged"() { diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherTest.groovy similarity index 79% rename from src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy rename to src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherTest.groovy index be55b2984a..7eaa9cec10 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherTest.groovy @@ -1,15 +1,10 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput -import graphql.ExecutionResult import graphql.GraphQL import graphql.TestUtil -import graphql.execution.AsyncExecutionStrategy import graphql.execution.AsyncSerialExecutionStrategy -import graphql.execution.ExecutionContext -import graphql.execution.ExecutionStrategyParameters import graphql.execution.instrumentation.ChainedInstrumentation -import graphql.execution.instrumentation.Instrumentation import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters @@ -29,17 +24,8 @@ import static graphql.StarWarsSchema.starWarsSchema import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring -class DataLoaderDispatcherInstrumentationTest extends Specification { +class DataLoaderDispatcherTest extends Specification { - class CaptureStrategy extends AsyncExecutionStrategy { - Instrumentation instrumentation = null - - @Override - CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - instrumentation = executionContext.instrumentation - return super.execute(executionContext, parameters) - } - } def query = """ query { @@ -65,36 +51,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { ] -// def "dataloader instrumentation is always added and an empty data loader registry is in place"() { -// -// def captureStrategy = new CaptureStrategy() -// def graphQL = GraphQL.newGraphQL(starWarsSchema).queryExecutionStrategy(captureStrategy) -// .instrumentation(new SimplePerformantInstrumentation()) -// .build() -// def executionInput = newExecutionInput().query('{ hero { name } }').build() -// when: -// graphQL.execute(executionInput) -// then: -// executionInput.getDataLoaderRegistry() != null -// def chainedInstrumentation = captureStrategy.instrumentation as ChainedInstrumentation -//// chainedInstrumentation.instrumentations.any { instr -> instr instanceof DataLoaderDispatcherInstrumentation } -// } - - def "dispatch is never called if data loader registry is not set"() { - def dataLoaderRegistry = new DataLoaderRegistry() { - @Override - void dispatchAll() { - assert false, "This should not be called when there are no data loaders" - } - } - def graphQL = GraphQL.newGraphQL(starWarsSchema).build() - def executionInput = newExecutionInput().query('{ hero { name } }').build() - when: - def er = graphQL.execute(executionInput) - then: - er.errors.isEmpty() - } def "dispatch is called if there are data loaders"() { def dispatchedCalled = false @@ -131,7 +88,6 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { DataLoaderRegistry startingDataLoaderRegistry = new DataLoaderRegistry() def enhancedDataLoaderRegistry = starWarsWiring.newDataLoaderRegistry() -// def dlInstrumentation = new DataLoaderDispatcherInstrumentation() def enhancingInstrumentation = new SimplePerformantInstrumentation() { @NotNull @@ -159,17 +115,15 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { @Unroll - def "ensure DataLoaderDispatcherInstrumentation works for #executionStrategyName"() { + def "ensure DataLoaderDispatcher works for #executionStrategyName"() { given: def starWarsWiring = new StarWarsDataLoaderWiring() def dlRegistry = starWarsWiring.newDataLoaderRegistry() -// def batchingInstrumentation = new DataLoaderDispatcherInstrumentation() def graphql = GraphQL.newGraphQL(starWarsWiring.schema) .queryExecutionStrategy(executionStrategy) -// .instrumentation(batchingInstrumentation) .build() when: diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy index ac8472830c..b816602cde 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/Issue1178DataLoaderDispatchTest.groovy @@ -75,8 +75,6 @@ class Issue1178DataLoaderDispatchTest extends Specification { when: def graphql = TestUtil.graphQL(sdl, wiring) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() then: "execution shouldn't error" diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy index 2bbaa27a9c..70bad946b0 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy @@ -183,7 +183,6 @@ class PeopleCompaniesAndProductsDataLoaderTest extends Specification { GraphQL graphQL = GraphQL .newGraphQL(graphQLSchema) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() when: diff --git a/src/test/groovy/readme/DataLoaderBatchingExamples.java b/src/test/groovy/readme/DataLoaderBatchingExamples.java index 24f5e1e7db..287d4c5650 100644 --- a/src/test/groovy/readme/DataLoaderBatchingExamples.java +++ b/src/test/groovy/readme/DataLoaderBatchingExamples.java @@ -82,13 +82,6 @@ public Object get(DataFetchingEnvironment environment) { // as each level of the graphql query is executed and hence make batched objects // available to the query and the associated DataFetchers // - // In this case we use options to make it keep statistics on the batching efficiency - // // - // DataLoaderDispatcherInstrumentationOptions options = DataLoaderDispatcherInstrumentationOptions - // .newOptions().includeStatistics(true); - // - // DataLoaderDispatcherInstrumentation dispatcherInstrumentation - // = new DataLoaderDispatcherInstrumentation(options); // // now build your graphql object and execute queries on it. @@ -96,7 +89,6 @@ public Object get(DataFetchingEnvironment environment) { // schema fields // GraphQL graphQL = GraphQL.newGraphQL(buildSchema()) - // .instrumentation(dispatcherInstrumentation) .build(); // From 58399b815232cd42bd4a8cdc59af06a0ebd0be1e Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 12:12:53 +1000 Subject: [PATCH 207/393] PR feedback and cleanup --- .../java/graphql/execution/AsyncExecutionStrategy.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 01aedcf8d6..eee639bb09 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -36,7 +36,8 @@ public AsyncExecutionStrategy(DataFetcherExceptionHandler exceptionHandler) { @Override @SuppressWarnings("FutureReturnValueIgnored") public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { - executionContext.getDataLoaderDispatcherStrategy().executionStrategy(executionContext, parameters); + DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = executionContext.getDataLoaderDispatcherStrategy(); + dataLoaderDispatcherStrategy.executionStrategy(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); @@ -64,14 +65,14 @@ public CompletableFuture execute(ExecutionContext executionCont for (FieldValueInfo completeValueInfo : completeValueInfos) { fieldValuesFutures.add(completeValueInfo.getFieldValueFuture()); } - executionContext.getDataLoaderDispatcherStrategy().executionStrategyOnFieldValuesInfo(completeValueInfos, parameters); + dataLoaderDispatcherStrategy.executionStrategyOnFieldValuesInfo(completeValueInfos, parameters); executionStrategyCtx.onFieldValuesInfo(completeValueInfos); fieldValuesFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. - executionContext.getDataLoaderDispatcherStrategy().executionStrategyOnFieldValuesException(ex, parameters); + dataLoaderDispatcherStrategy.executionStrategyOnFieldValuesException(ex, parameters); executionStrategyCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; From d34ec293135b95f5d75ede442527da8d54565743 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 12:13:54 +1000 Subject: [PATCH 208/393] PR feedback and cleanup --- .../instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy index 4ffd8c1199..03b60e4e39 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderTypeMismatchTest.groovy @@ -62,7 +62,6 @@ class DataLoaderTypeMismatchTest extends Specification { def schema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring) def graphql = GraphQL.newGraphQL(schema) -// .instrumentation(new DataLoaderDispatcherInstrumentation()) .build() when: From 8389bddd0e35a9c571a1b3328a0647a5ff21d814 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 12:34:37 +1000 Subject: [PATCH 209/393] use ReentrantLock instead of synchronize --- .../PerLevelDataLoaderDispatchStrategy.java | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index 26a00fadf7..9ac4e0d181 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -8,6 +8,7 @@ import graphql.execution.FieldValueInfo; import graphql.execution.MergedField; import graphql.schema.DataFetcher; +import graphql.util.LockKit; import org.dataloader.DataLoaderRegistry; import java.util.LinkedHashSet; @@ -21,7 +22,10 @@ public class PerLevelDataLoaderDispatchStrategy implements DataLoaderDispatchStr private final CallStack callStack; private final ExecutionContext executionContext; + private static class CallStack { + + private final LockKit.ReentrantLock lock = new LockKit.ReentrantLock(); private final LevelMap expectedFetchCountPerLevel = new LevelMap(); private final LevelMap fetchCountPerLevel = new LevelMap(); private final LevelMap expectedStrategyCallsPerLevel = new LevelMap(); @@ -69,13 +73,13 @@ boolean allFetchesHappened(int level) { @Override public String toString() { return "CallStack{" + - "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + - ", fetchCountPerLevel=" + fetchCountPerLevel + - ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + - ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + - ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + - ", dispatchedLevels" + dispatchedLevels + - '}'; + "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + + ", fetchCountPerLevel=" + fetchCountPerLevel + + ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + + ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + + ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + + ", dispatchedLevels" + dispatchedLevels + + '}'; } @@ -113,9 +117,9 @@ public void executionStrategyOnFieldValuesInfo(List fieldValueIn public void executionStrategyOnFieldValuesException(Throwable t, ExecutionStrategyParameters executionStrategyParameters) { int curLevel = executionStrategyParameters.getPath().getLevel() + 1; - synchronized (callStack) { - callStack.increaseHappenedOnFieldValueCalls(curLevel); - } + callStack.lock.runLocked(() -> + callStack.increaseHappenedOnFieldValueCalls(curLevel) + ); } @@ -135,34 +139,31 @@ public void executeObjectOnFieldValuesInfo(List fieldValueInfoLi @Override public void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyParameters parameters) { int curLevel = parameters.getPath().getLevel() + 1; - synchronized (callStack) { - callStack.increaseHappenedOnFieldValueCalls(curLevel); - } + callStack.lock.runLocked(() -> + callStack.increaseHappenedOnFieldValueCalls(curLevel) + ); } - @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") private void increaseCallCounts(int curLevel, ExecutionStrategyParameters executionStrategyParameters) { int fieldCount = executionStrategyParameters.getFields().size(); - synchronized (callStack) { + callStack.lock.runLocked(() -> { callStack.increaseExpectedFetchCount(curLevel, fieldCount); callStack.increaseHappenedStrategyCalls(curLevel); - } + }); } - @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") private void onFieldValuesInfoDispatchIfNeeded(List fieldValueInfoList, int curLevel, ExecutionStrategyParameters parameters) { - boolean dispatchNeeded; - synchronized (callStack) { - dispatchNeeded = handleOnFieldValuesInfo(fieldValueInfoList, curLevel); - } + boolean dispatchNeeded = callStack.lock.callLocked(() -> + handleOnFieldValuesInfo(fieldValueInfoList, curLevel) + ); if (dispatchNeeded) { dispatch(curLevel); } } // -// thread safety: called with synchronised(callStack) +// thread safety: called with callStack.lock // private boolean handleOnFieldValuesInfo(List fieldValueInfos, int curLevel) { callStack.increaseHappenedOnFieldValueCalls(curLevel); @@ -187,11 +188,10 @@ private int getCountForList(List fieldValueInfos) { @Override public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, DataFetcher dataFetcher, CompletableFuture fetchedValue) { int level = executionStrategyParameters.getPath().getLevel(); - boolean dispatchNeeded; - synchronized (callStack) { + boolean dispatchNeeded = callStack.lock.callLocked(() -> { callStack.increaseFetchCount(level); - dispatchNeeded = dispatchIfNeeded(level); - } + return dispatchIfNeeded(level); + }); if (dispatchNeeded) { dispatch(level); } @@ -200,7 +200,7 @@ public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyPar // -// thread safety : called with synchronised(callStack) +// thread safety : called with callStack.lock // private boolean dispatchIfNeeded(int level) { boolean ready = levelReady(level); @@ -211,7 +211,7 @@ private boolean dispatchIfNeeded(int level) { } // -// thread safety: called with synchronised(callStack) +// thread safety: called with callStack.lock // private boolean levelReady(int level) { if (level == 1) { @@ -219,7 +219,7 @@ private boolean levelReady(int level) { return callStack.allFetchesHappened(1); } if (levelReady(level - 1) && callStack.allOnFieldCallsHappened(level - 1) - && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { + && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { return true; } From be376f6809cfccbe653b3e0c386f89faba14d6e3 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 12:39:29 +1000 Subject: [PATCH 210/393] formatting --- src/main/java/graphql/GraphQL.java | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index a076741de3..c43e6dec29 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -171,6 +171,7 @@ public ValueUnboxer getValueUnboxer() { * 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) { @@ -182,17 +183,18 @@ public static Builder newGraphQL(GraphQLSchema graphQLSchema) { * 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(this.queryStrategy) - .mutationExecutionStrategy(this.mutationStrategy) - .subscriptionExecutionStrategy(this.subscriptionStrategy) - .executionIdProvider(Optional.ofNullable(this.idProvider).orElse(builder.idProvider)) - .instrumentation(Optional.ofNullable(this.instrumentation).orElse(builder.instrumentation)) - .preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider)); + .queryExecutionStrategy(this.queryStrategy) + .mutationExecutionStrategy(this.mutationStrategy) + .subscriptionExecutionStrategy(this.subscriptionStrategy) + .executionIdProvider(Optional.ofNullable(this.idProvider).orElse(builder.idProvider)) + .instrumentation(Optional.ofNullable(this.instrumentation).orElse(builder.instrumentation)) + .preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider)); builderConsumer.accept(builder); @@ -242,6 +244,7 @@ public Builder subscriptionExecutionStrategy(ExecutionStrategy executionStrategy * in {@link graphql.schema.DataFetcher} invocations. * * @param dataFetcherExceptionHandler the default handler for data fetching exception + * * @return this builder */ public Builder defaultDataFetcherExceptionHandler(DataFetcherExceptionHandler dataFetcherExceptionHandler) { @@ -304,12 +307,13 @@ public GraphQL build() { * 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(); + .query(query) + .build(); return execute(executionInput); } @@ -318,6 +322,7 @@ public ExecutionResult execute(String query) { * 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) { @@ -335,6 +340,7 @@ public ExecutionResult execute(ExecutionInput.Builder executionInputBuilder) { * * * @param builderFunction a function that is given a {@link ExecutionInput.Builder} + * * @return an {@link ExecutionResult} which can include errors */ public ExecutionResult execute(UnaryOperator builderFunction) { @@ -345,6 +351,7 @@ public ExecutionResult execute(UnaryOperator builderFunc * 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) { @@ -366,6 +373,7 @@ public ExecutionResult execute(ExecutionInput executionInput) { * 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) { @@ -386,6 +394,7 @@ public CompletableFuture executeAsync(ExecutionInput.Builder ex * * * @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) { @@ -399,6 +408,7 @@ public CompletableFuture executeAsync(UnaryOperator executeAsync(ExecutionInput executionInput) { @@ -511,7 +521,7 @@ private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchem DocumentAndVariables documentAndVariables = parseResult.getDocumentAndVariables(); documentAndVariables = instrumentation.instrumentDocumentAndVariables(documentAndVariables, parameters, instrumentationState); return ParseAndValidateResult.newResult() - .document(documentAndVariables.getDocument()).variables(documentAndVariables.getVariables()).build(); + .document(documentAndVariables.getDocument()).variables(documentAndVariables.getVariables()).build(); } } From 6bf834b63fbbf3af411fa1114c69197a2dea18f0 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 12:42:22 +1000 Subject: [PATCH 211/393] cleanup --- .../graphql/execution/AsyncSerialExecutionStrategy.java | 4 ++-- src/main/java/graphql/execution/ExecutionStrategy.java | 7 ++++--- ...aLoaderPerformanceWithChainedInstrumentationTest.groovy | 3 --- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index d009b99e4c..0a6c7c51db 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -35,7 +35,7 @@ public CompletableFuture execute(ExecutionContext executionCont Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); InstrumentationContext executionStrategyCtx = nonNullCtx(instrumentation.beginExecutionStrategy(instrumentationParameters, - executionContext.getInstrumentationState()) + executionContext.getInstrumentationState()) ); MergedSelectionSet fields = parameters.getFields(); ImmutableList fieldNames = ImmutableList.copyOf(fields.keySet()); @@ -44,7 +44,7 @@ public CompletableFuture execute(ExecutionContext executionCont MergedField currentField = fields.getSubField(fieldName); ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); ExecutionStrategyParameters newParameters = parameters - .transform(builder -> builder.field(currentField).path(fieldPath)); + .transform(builder -> builder.field(currentField).path(fieldPath)); return resolveField(executionContext, newParameters); }); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 156f07cadc..0d79d326b8 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -196,7 +196,8 @@ public static String mkNameForPath(List currentField) { * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ protected CompletableFuture> executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { - executionContext.getDataLoaderDispatcherStrategy().executeObject(executionContext, parameters); + DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = executionContext.getDataLoaderDispatcherStrategy(); + dataLoaderDispatcherStrategy.executeObject(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationExecutionStrategyParameters instrumentationParameters = new InstrumentationExecutionStrategyParameters(executionContext, parameters); @@ -225,14 +226,14 @@ protected CompletableFuture> executeObject(ExecutionContext for (FieldValueInfo completeValueInfo : completeValueInfos) { resultFutures.add(completeValueInfo.getFieldValueFuture()); } - executionContext.getDataLoaderDispatcherStrategy().executeObjectOnFieldValuesInfo(completeValueInfos, parameters); + dataLoaderDispatcherStrategy.executeObjectOnFieldValuesInfo(completeValueInfos, parameters); resolveObjectCtx.onFieldValuesInfo(completeValueInfos); resultFutures.await().whenComplete(handleResultsConsumer); }).exceptionally((ex) -> { // if there are any issues with combining/handling the field results, // complete the future at all costs and bubble up any thrown exception so // the execution does not hang. - executionContext.getDataLoaderDispatcherStrategy().executeObjectOnFieldValuesException(ex, parameters); + dataLoaderDispatcherStrategy.executeObjectOnFieldValuesException(ex, parameters); resolveObjectCtx.onFieldValuesException(); overallResult.completeExceptionally(ex); return null; diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy index 82957cd489..e5d1089bab 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderPerformanceWithChainedInstrumentationTest.groovy @@ -31,9 +31,6 @@ class DataLoaderPerformanceWithChainedInstrumentationTest extends Specification DataLoaderPerformanceData dataLoaderPerformanceData = new DataLoaderPerformanceData(batchCompareDataFetchers) dataLoaderRegistry = dataLoaderPerformanceData.setupDataLoaderRegistry() -// Instrumentation instrumentation = new ChainedInstrumentation( -// Collections.singletonList(new DataLoaderDispatcherInstrumentation()) -// ) graphQL = dataLoaderPerformanceData.setupGraphQL() } From aa63d8981e06d82d1dd4edefffc4b483f64a1725 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 13:20:01 +1000 Subject: [PATCH 212/393] move the Tracking data into GJ and assert in tests --- .../src/test/java/graphql/test/AgentTest.java | 46 ++- .../src/test/java/graphql/test/TestQuery.java | 1 - .../java/graphql/agent/GraphQLJavaAgent.java | 297 ++++++------------ .../agent/result/ExecutionTrackingResult.java | 108 ++++++- 4 files changed, 220 insertions(+), 232 deletions(-) diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java index dd90eed174..8a11911a26 100644 --- a/agent-test/src/test/java/graphql/test/AgentTest.java +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -5,6 +5,10 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + public class AgentTest { @BeforeAll @@ -19,23 +23,39 @@ static void cleanup() { @Test void test() { ExecutionTrackingResult executionTrackingResult = TestQuery.executeQuery(); - // assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(5); - // assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); - // assertThat(executionTrackingResult.getDfResultTypes("/issues")) - // .isEqualTo(ExecutionTrackingResult.DFResultType.DONE_OK); + assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(5); + assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); + assertThat(executionTrackingResult.getDfResultTypes("/issues")) + .isEqualTo(ExecutionTrackingResult.DFResultType.DONE_OK); + + verifyAgentDataIsEmpty(); + } @Test void testBatchLoader() { ExecutionTrackingResult executionTrackingResult = TestQuery.executeBatchedQuery(); - TestQuery.executeBatchedQuery(); - TestQuery.executeBatchedQuery(); - TestQuery.executeBatchedQuery(); - // assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(9); - // assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); - // assertThat(executionTrackingResult.getDfResultTypes("/issues[0]/author")) - // .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); - // assertThat(executionTrackingResult.getDfResultTypes("/issues[1]/author")) - // .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); + assertThat(executionTrackingResult.dataFetcherCount()).isEqualTo(9); + assertThat(executionTrackingResult.getTime("/issues")).isGreaterThan(100); + assertThat(executionTrackingResult.getDfResultTypes("/issues[0]/author")) + .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); + assertThat(executionTrackingResult.getDfResultTypes("/issues[1]/author")) + .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); + + verifyAgentDataIsEmpty(); + } + + private void verifyAgentDataIsEmpty() { + try { + Class agent = Class.forName("graphql.agent.GraphQLJavaAgent"); + Map executionIdToData = (Map) agent.getField("executionIdToData").get(null); + Map dataLoaderToExecutionId = (Map) agent.getField("dataLoaderToExecutionId").get(null); + assertThat(executionIdToData).isEmpty(); + assertThat(dataLoaderToExecutionId).isEmpty(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } } diff --git a/agent-test/src/test/java/graphql/test/TestQuery.java b/agent-test/src/test/java/graphql/test/TestQuery.java index a518ff3eed..2755cf230f 100644 --- a/agent-test/src/test/java/graphql/test/TestQuery.java +++ b/agent-test/src/test/java/graphql/test/TestQuery.java @@ -94,7 +94,6 @@ static ExecutionTrackingResult executeBatchedQuery() { .query(query).build(); ExecutionResult result = graphQL.execute(executionInput); Assertions.assertThat(result.getErrors()).isEmpty(); - System.out.println("result: " + result.getData()); ExecutionTrackingResult trackingResult = executionInput.getGraphQLContext().get(ExecutionTrackingResult.EXECUTION_TRACKING_KEY); return trackingResult; } diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index a827a304b5..8a27cd8a20 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -20,14 +20,12 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; -import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_CANCELLED; -import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_EXCEPTIONALLY; -import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.DONE_OK; -import static graphql.agent.GraphQLJavaAgent.ExecutionData.DFResultType.PENDING; +import static graphql.agent.result.ExecutionTrackingResult.DFResultType.DONE_CANCELLED; +import static graphql.agent.result.ExecutionTrackingResult.DFResultType.DONE_EXCEPTIONALLY; +import static graphql.agent.result.ExecutionTrackingResult.DFResultType.DONE_OK; +import static graphql.agent.result.ExecutionTrackingResult.DFResultType.PENDING; import static graphql.agent.result.ExecutionTrackingResult.EXECUTION_TRACKING_KEY; import static net.bytebuddy.matcher.ElementMatchers.nameMatches; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -35,177 +33,54 @@ public class GraphQLJavaAgent { - public static class ExecutionData { - public final AtomicReference startThread = new AtomicReference<>(); - public final AtomicReference endThread = new AtomicReference<>(); - public final AtomicLong startExecutionTime = new AtomicLong(); - public final AtomicLong endExecutionTime = new AtomicLong(); - public final Map resultPathToDataLoaderUsed = new ConcurrentHashMap<>(); - public final Map dataLoaderToName = new ConcurrentHashMap<>(); - - public final Map timePerPath = new ConcurrentHashMap<>(); - public final Map finishedTimePerPath = new ConcurrentHashMap<>(); - public final Map finishedThreadPerPath = new ConcurrentHashMap<>(); - public final Map startInvocationThreadPerPath = new ConcurrentHashMap<>(); - private final Map dfResultTypes = new ConcurrentHashMap<>(); - - public static class BatchLoadingCall { - public BatchLoadingCall(int keyCount, String threadName) { - this.keyCount = keyCount; - this.threadName = threadName; - } - - public final int keyCount; - public final String threadName; - } - - public final Map> dataLoaderNameToBatchCall = new ConcurrentHashMap<>(); - - public String print(String executionId) { - StringBuilder s = new StringBuilder(); - s.append("==========================").append("\n"); - s.append("Summary for execution with id ").append(executionId).append("\n"); - s.append("==========================").append("\n"); - s.append("Execution time in ms:").append((endExecutionTime.get() - startExecutionTime.get()) / 1_000_000L).append("\n"); - s.append("Fields count: ").append(timePerPath.keySet().size()).append("\n"); - s.append("Blocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType != PENDING).count()).append("\n"); - s.append("Nonblocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType == PENDING).count()).append("\n"); - s.append("DataLoaders used: ").append(dataLoaderToName.size()).append("\n"); - s.append("DataLoader names: ").append(dataLoaderToName.values()).append("\n"); - s.append("start execution thread: '").append(startThread.get()).append("'\n"); - s.append("end execution thread: '").append(endThread.get()).append("'\n"); - s.append("BatchLoader calls details: ").append("\n"); - s.append("==========================").append("\n"); - for (String dataLoaderName : dataLoaderNameToBatchCall.keySet()) { - s.append("Batch call: '").append(dataLoaderName).append("' made ").append(dataLoaderNameToBatchCall.get(dataLoaderName).size()).append(" times, ").append("\n"); - for (BatchLoadingCall batchLoadingCall : dataLoaderNameToBatchCall.get(dataLoaderName)) { - s.append("Batch call with ").append(batchLoadingCall.keyCount).append(" keys ").append(" in thread ").append(batchLoadingCall.threadName).append("\n"); - } - List resultPathUsed = new ArrayList<>(); - for (ResultPath resultPath : resultPathToDataLoaderUsed.keySet()) { - if (resultPathToDataLoaderUsed.get(resultPath).equals(dataLoaderName)) { - resultPathUsed.add(resultPath); - } - } - s.append("DataLoader: '").append(dataLoaderName).append("' used in fields: ").append(resultPathUsed).append("\n"); - } - s.append("Field details:").append("\n"); - s.append("===============").append("\n"); - for (ResultPath path : timePerPath.keySet()) { - s.append("Field: '").append(path).append("'\n"); - s.append("invocation time: ").append(timePerPath.get(path)).append(" nano seconds, ").append("\n"); - s.append("completion time: ").append(finishedTimePerPath.get(path)).append(" nano seconds, ").append("\n"); - s.append("Result type: ").append(dfResultTypes.get(path)).append("\n"); - s.append("invoked in thread: ").append(startInvocationThreadPerPath.get(path)).append("\n"); - s.append("finished in thread: ").append(finishedThreadPerPath.get(path)).append("\n"); - s.append("-------------\n"); - } - s.append("==========================").append("\n"); - s.append("==========================").append("\n"); - return s.toString(); - - } - - @Override - public String toString() { - return "ExecutionData{" + - "resultPathToDataLoaderUsed=" + resultPathToDataLoaderUsed + - ", dataLoaderNames=" + dataLoaderToName.values() + - ", timePerPath=" + timePerPath + - ", dfResultTypes=" + dfResultTypes + - '}'; - } - - public enum DFResultType { - DONE_OK, - DONE_EXCEPTIONALLY, - DONE_CANCELLED, - PENDING, - } - - - public void start(ResultPath path, long startTime) { - timePerPath.put(path, startTime); - } - - public void end(ResultPath path, long endTime) { - timePerPath.put(path, endTime - timePerPath.get(path)); - } - - public int dataFetcherCount() { - return timePerPath.size(); - } - - public long getTime(ResultPath path) { - return timePerPath.get(path); - } - - public long getTime(String path) { - return timePerPath.get(ResultPath.parse(path)); - } - - public void setDfResultTypes(ResultPath resultPath, DFResultType resultTypes) { - dfResultTypes.put(resultPath, resultTypes); - } - public DFResultType getDfResultTypes(ResultPath resultPath) { - return dfResultTypes.get(resultPath); - } - - public DFResultType getDfResultTypes(String resultPath) { - return dfResultTypes.get(ResultPath.parse(resultPath)); - } - - - } - - public static final Map executionIdToData = new ConcurrentHashMap<>(); + public static final Map executionIdToData = new ConcurrentHashMap<>(); public static final Map dataLoaderToExecutionId = new ConcurrentHashMap<>(); public static void agentmain(String agentArgs, Instrumentation inst) { - System.out.println("Agent is running"); + System.out.println("GraphQL Java Agent is starting"); new AgentBuilder.Default() - .type(named("graphql.execution.Execution")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // ClassInjector.UsingInstrumentation.of() - // System.out.println("transforming " + typeDescription); - return builder - .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); - - }) - .type(named("graphql.execution.ExecutionStrategy")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - return builder - .visit(Advice.to(DataFetcherInvokeAdvice.class).on(nameMatches("invokeDataFetcher"))); - }) - .type(named("org.dataloader.DataLoaderRegistry")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); - return builder - .visit(Advice.to(DataLoaderRegistryAdvice.class).on(nameMatches("dispatchAll"))); - }) - .type(named("org.dataloader.DataLoader")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); - return builder - .visit(Advice.to(DataLoaderLoadAdvice.class).on(nameMatches("load"))); - }) - .type(named("org.dataloader.DataLoaderHelper")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); - return builder - .visit(Advice.to(DataLoaderHelperDispatchAdvice.class).on(nameMatches("dispatch"))) - .visit(Advice.to(DataLoaderHelperInvokeBatchLoaderAdvice.class) - .on(nameMatches("invokeLoader").and(takesArguments(List.class, List.class)))); - }) - .type(named("graphql.schema.DataFetchingEnvironmentImpl")) - .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); - return builder - .visit(Advice.to(DataFetchingEnvironmentAdvice.class).on(nameMatches("getDataLoader"))); - }) - .disableClassFormatChanges() - .installOn(inst); + .type(named("graphql.execution.Execution")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + // ClassInjector.UsingInstrumentation.of() + // System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); + + }) + .type(named("graphql.execution.ExecutionStrategy")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + return builder + .visit(Advice.to(DataFetcherInvokeAdvice.class).on(nameMatches("invokeDataFetcher"))); + }) + .type(named("org.dataloader.DataLoaderRegistry")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + // System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(DataLoaderRegistryAdvice.class).on(nameMatches("dispatchAll"))); + }) + .type(named("org.dataloader.DataLoader")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + // System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(DataLoaderLoadAdvice.class).on(nameMatches("load"))); + }) + .type(named("org.dataloader.DataLoaderHelper")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + // System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(DataLoaderHelperDispatchAdvice.class).on(nameMatches("dispatch"))) + .visit(Advice.to(DataLoaderHelperInvokeBatchLoaderAdvice.class) + .on(nameMatches("invokeLoader").and(takesArguments(List.class, List.class)))); + }) + .type(named("graphql.schema.DataFetchingEnvironmentImpl")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { + // System.out.println("transforming " + typeDescription); + return builder + .visit(Advice.to(DataFetchingEnvironmentAdvice.class).on(nameMatches("getDataLoader"))); + }) + .disableClassFormatChanges() + .installOn(inst); } @@ -221,11 +96,16 @@ public AfterExecutionHandler(ExecutionContext executionContext) { public void accept(Object o, Throwable throwable) { ExecutionId executionId = executionContext.getExecutionId(); - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); - executionData.endExecutionTime.set(System.nanoTime()); - executionData.endThread.set(Thread.currentThread().getName()); - System.out.println("execution finished for: " + executionId + " with data " + executionData); - System.out.println(executionData.print(executionId.toString())); + ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(executionId); + executionTrackingResult.endExecutionTime.set(System.nanoTime()); + executionTrackingResult.endThread.set(Thread.currentThread().getName()); + executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, executionTrackingResult); + // cleanup + for (DataLoader dataLoader : executionTrackingResult.dataLoaderToName.keySet()) { + dataLoaderToExecutionId.remove(dataLoader); + } + executionIdToData.remove(executionId); + } } @@ -233,19 +113,18 @@ public void accept(Object o, Throwable throwable) { @Advice.OnMethodEnter public static void executeOperationEnter(@Advice.Argument(0) ExecutionContext executionContext) { - GraphQLJavaAgent.ExecutionData executionData = new GraphQLJavaAgent.ExecutionData(); - executionData.startExecutionTime.set(System.nanoTime()); - executionData.startThread.set(Thread.currentThread().getName()); - System.out.println("execution started for: " + executionContext.getExecutionId()); + ExecutionTrackingResult executionTrackingResult = new ExecutionTrackingResult(); + executionTrackingResult.startExecutionTime.set(System.nanoTime()); + executionTrackingResult.startThread.set(Thread.currentThread().getName()); executionContext.getGraphQLContext().put(EXECUTION_TRACKING_KEY, new ExecutionTrackingResult()); - GraphQLJavaAgent.executionIdToData.put(executionContext.getExecutionId(), executionData); + GraphQLJavaAgent.executionIdToData.put(executionContext.getExecutionId(), executionTrackingResult); DataLoaderRegistry dataLoaderRegistry = executionContext.getDataLoaderRegistry(); for (String name : dataLoaderRegistry.getDataLoadersMap().keySet()) { DataLoader dataLoader = dataLoaderRegistry.getDataLoader(name); GraphQLJavaAgent.dataLoaderToExecutionId.put(dataLoader, executionContext.getExecutionId()); - executionData.dataLoaderToName.put(dataLoader, name); + executionTrackingResult.dataLoaderToName.put(dataLoader, name); } } @@ -274,19 +153,19 @@ public DataFetcherFinishedHandler(ExecutionContext executionContext, ExecutionSt @Override public void accept(Object o, Throwable throwable) { ExecutionId executionId = executionContext.getExecutionId(); - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); + ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(executionId); ResultPath path = parameters.getPath(); - executionData.finishedTimePerPath.put(path, System.nanoTime() - startTime); - executionData.finishedThreadPerPath.put(path, Thread.currentThread().getName()); + executionTrackingResult.finishedTimePerPath.put(path, System.nanoTime() - startTime); + executionTrackingResult.finishedThreadPerPath.put(path, Thread.currentThread().getName()); } } @Advice.OnMethodEnter public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext executionContext, @Advice.Argument(1) ExecutionStrategyParameters parameters) { - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); - executionData.start(parameters.getPath(), System.nanoTime()); - executionData.startInvocationThreadPerPath.put(parameters.getPath(), Thread.currentThread().getName()); + ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); + executionTrackingResult.start(parameters.getPath(), System.nanoTime()); + executionTrackingResult.startInvocationThreadPerPath.put(parameters.getPath(), Thread.currentThread().getName()); } @Advice.OnMethodExit @@ -294,20 +173,20 @@ public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext ex @Advice.Argument(1) ExecutionStrategyParameters parameters, @Advice.Return(readOnly = false) CompletableFuture result) { // ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); + ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); ResultPath path = parameters.getPath(); - long startTime = executionData.timePerPath.get(path); - executionData.end(path, System.nanoTime()); + long startTime = executionTrackingResult.timePerPath.get(path); + executionTrackingResult.end(path, System.nanoTime()); if (result.isDone()) { if (result.isCancelled()) { - executionData.setDfResultTypes(path, DONE_CANCELLED); + executionTrackingResult.setDfResultTypes(path, DONE_CANCELLED); } else if (result.isCompletedExceptionally()) { - executionData.setDfResultTypes(path, DONE_EXCEPTIONALLY); + executionTrackingResult.setDfResultTypes(path, DONE_EXCEPTIONALLY); } else { - executionData.setDfResultTypes(path, DONE_OK); + executionTrackingResult.setDfResultTypes(path, DONE_OK); } } else { - executionData.setDfResultTypes(path, PENDING); + executionTrackingResult.setDfResultTypes(path, PENDING); } // overriding the result to make sure the finished handler is called first when the DF is finished // otherwise it is a completion tree instead of chain @@ -325,13 +204,13 @@ public static void invokeLoader(@Advice.Argument(0) List keys, @Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoaderHelper) { DataLoader dataLoader = getDataLoaderForHelper(dataLoaderHelper); ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); - ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); - String dataLoaderName = executionData.dataLoaderToName.get(dataLoader); + ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(executionId); + String dataLoaderName = executionTrackingResult.dataLoaderToName.get(dataLoader); - synchronized (executionData.dataLoaderNameToBatchCall) { - executionData.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); - executionData.dataLoaderNameToBatchCall.get(dataLoaderName) - .add(new GraphQLJavaAgent.ExecutionData.BatchLoadingCall(keys.size(), Thread.currentThread().getName())); + synchronized (executionTrackingResult.dataLoaderNameToBatchCall) { + executionTrackingResult.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); + executionTrackingResult.dataLoaderNameToBatchCall.get(dataLoaderName) + .add(new ExecutionTrackingResult.BatchLoadingCall(keys.size(), Thread.currentThread().getName())); } } @@ -347,11 +226,11 @@ public static void dispatch(@Advice.This(typing = Assigner.Typing.DYNAMIC) Objec // DataLoader dataLoader = getDataLoaderForHelper(dataLoaderHelper); // // System.out.println("dataLoader: " + dataLoader); // ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); - // GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(executionId); - // String dataLoaderName = executionData.dataLoaderToName.get(dataLoader); + // ExecutionTrackingResult ExecutionTrackingResult = GraphQLJavaAgent.executionIdToData.get(executionId); + // String dataLoaderName = ExecutionTrackingResult.dataLoaderToName.get(dataLoader); // - // executionData.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); - // executionData.dataLoaderNameToBatchCall.get(dataLoaderName).add(new GraphQLJavaAgent.ExecutionData.BatchLoadingCall(dispatchResult.getKeysCount())); + // ExecutionTrackingResult.dataLoaderNameToBatchCall.putIfAbsent(dataLoaderName, new ArrayList<>()); + // ExecutionTrackingResult.dataLoaderNameToBatchCall.get(dataLoaderName).add(new ExecutionTrackingResult.BatchLoadingCall(dispatchResult.getKeysCount())); } catch (Exception e) { e.printStackTrace(); @@ -382,10 +261,10 @@ class DataFetchingEnvironmentAdvice { public static void getDataLoader(@Advice.Argument(0) String dataLoaderName, @Advice.This(typing = Assigner.Typing.DYNAMIC) DataFetchingEnvironment dataFetchingEnvironment, @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) DataLoader dataLoader) { - GraphQLJavaAgent.ExecutionData executionData = GraphQLJavaAgent.executionIdToData.get(dataFetchingEnvironment.getExecutionId()); - // System.out.println("execution data: " + executionData); + ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(dataFetchingEnvironment.getExecutionId()); + // System.out.println("execution data: " + ExecutionTrackingResult); ResultPath resultPath = dataFetchingEnvironment.getExecutionStepInfo().getPath(); - executionData.resultPathToDataLoaderUsed.put(resultPath, dataLoaderName); + executionTrackingResult.resultPathToDataLoaderUsed.put(resultPath, dataLoaderName); // System.out.println(dataLoaderName + " > " + dataLoader); } diff --git a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java index bcc297e5e0..77c03def75 100644 --- a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java +++ b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java @@ -1,12 +1,110 @@ package graphql.agent.result; +import graphql.PublicApi; import graphql.execution.ResultPath; +import org.dataloader.DataLoader; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import static graphql.agent.result.ExecutionTrackingResult.DFResultType.PENDING; + +/** + * This is the result of the agent tracking an execution. + * It can be found inside the GraphQLContext after the execution with the key {@link ExecutionTrackingResult#EXECUTION_TRACKING_KEY} + * + * Note: While this is public API, the main goal is temporary debugging to understand an execution better with minimal overhead. + * Therefore this will evolve over time if needed to be performant and reflect the overall execution. + * It is not recommended to have the agent on always or to rely on this class during normal execution + */ +@PublicApi public class ExecutionTrackingResult { + public static final String EXECUTION_TRACKING_KEY = "__GJ_AGENT_EXECUTION_TRACKING"; + public final AtomicReference startThread = new AtomicReference<>(); + public final AtomicReference endThread = new AtomicReference<>(); + public final AtomicLong startExecutionTime = new AtomicLong(); + public final AtomicLong endExecutionTime = new AtomicLong(); + public final Map resultPathToDataLoaderUsed = new ConcurrentHashMap<>(); + public final Map dataLoaderToName = new ConcurrentHashMap<>(); + + public final Map timePerPath = new ConcurrentHashMap<>(); + public final Map finishedTimePerPath = new ConcurrentHashMap<>(); + public final Map finishedThreadPerPath = new ConcurrentHashMap<>(); + public final Map startInvocationThreadPerPath = new ConcurrentHashMap<>(); + private final Map dfResultTypes = new ConcurrentHashMap<>(); + + public static class BatchLoadingCall { + public BatchLoadingCall(int keyCount, String threadName) { + this.keyCount = keyCount; + this.threadName = threadName; + } + + public final int keyCount; + public final String threadName; + } + + public final Map> dataLoaderNameToBatchCall = new ConcurrentHashMap<>(); + + public String print(String executionId) { + StringBuilder s = new StringBuilder(); + s.append("==========================").append("\n"); + s.append("Summary for execution with id ").append(executionId).append("\n"); + s.append("==========================").append("\n"); + s.append("Execution time in ms:").append((endExecutionTime.get() - startExecutionTime.get()) / 1_000_000L).append("\n"); + s.append("Fields count: ").append(timePerPath.keySet().size()).append("\n"); + s.append("Blocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType != PENDING).count()).append("\n"); + s.append("Nonblocking fields count: ").append(dfResultTypes.values().stream().filter(dfResultType -> dfResultType == PENDING).count()).append("\n"); + s.append("DataLoaders used: ").append(dataLoaderToName.size()).append("\n"); + s.append("DataLoader names: ").append(dataLoaderToName.values()).append("\n"); + s.append("start execution thread: '").append(startThread.get()).append("'\n"); + s.append("end execution thread: '").append(endThread.get()).append("'\n"); + s.append("BatchLoader calls details: ").append("\n"); + s.append("==========================").append("\n"); + for (String dataLoaderName : dataLoaderNameToBatchCall.keySet()) { + s.append("Batch call: '").append(dataLoaderName).append("' made ").append(dataLoaderNameToBatchCall.get(dataLoaderName).size()).append(" times, ").append("\n"); + for (BatchLoadingCall batchLoadingCall : dataLoaderNameToBatchCall.get(dataLoaderName)) { + s.append("Batch call with ").append(batchLoadingCall.keyCount).append(" keys ").append(" in thread ").append(batchLoadingCall.threadName).append("\n"); + } + List resultPathUsed = new ArrayList<>(); + for (ResultPath resultPath : resultPathToDataLoaderUsed.keySet()) { + if (resultPathToDataLoaderUsed.get(resultPath).equals(dataLoaderName)) { + resultPathUsed.add(resultPath); + } + } + s.append("DataLoader: '").append(dataLoaderName).append("' used in fields: ").append(resultPathUsed).append("\n"); + } + s.append("Field details:").append("\n"); + s.append("===============").append("\n"); + for (ResultPath path : timePerPath.keySet()) { + s.append("Field: '").append(path).append("'\n"); + s.append("invocation time: ").append(timePerPath.get(path)).append(" nano seconds, ").append("\n"); + s.append("completion time: ").append(finishedTimePerPath.get(path)).append(" nano seconds, ").append("\n"); + s.append("Result type: ").append(dfResultTypes.get(path)).append("\n"); + s.append("invoked in thread: ").append(startInvocationThreadPerPath.get(path)).append("\n"); + s.append("finished in thread: ").append(finishedThreadPerPath.get(path)).append("\n"); + s.append("-------------\n"); + } + s.append("==========================").append("\n"); + s.append("==========================").append("\n"); + return s.toString(); + + } + + @Override + public String toString() { + return "ExecutionData{" + + "resultPathToDataLoaderUsed=" + resultPathToDataLoaderUsed + + ", dataLoaderNames=" + dataLoaderToName.values() + + ", timePerPath=" + timePerPath + + ", dfResultTypes=" + dfResultTypes + + '}'; + } + public enum DFResultType { DONE_OK, DONE_EXCEPTIONALLY, @@ -14,9 +112,6 @@ public enum DFResultType { PENDING, } - public static final String EXECUTION_TRACKING_KEY = "__GJ_AGENT_EXECUTION_TRACKING"; - private final Map timePerPath = new ConcurrentHashMap<>(); - private final Map dfResultTypes = new ConcurrentHashMap<>(); public void start(ResultPath path, long startTime) { timePerPath.put(path, startTime); @@ -50,10 +145,5 @@ public DFResultType getDfResultTypes(String resultPath) { return dfResultTypes.get(ResultPath.parse(resultPath)); } - @Override - public String toString() { - return "ExecutionTrackingResult{" + - "timePerPath=" + timePerPath + - '}'; - } + } From f04514c11e0fbd52001f930b7930be1219ab7a3c Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 19:23:47 +1000 Subject: [PATCH 213/393] formatting --- .../ExecutionStrategyEquivalenceTest.groovy | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyEquivalenceTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyEquivalenceTest.groovy index 17d998699a..93e58e3aa6 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyEquivalenceTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyEquivalenceTest.groovy @@ -6,8 +6,6 @@ import graphql.StarWarsSchema import spock.lang.Specification import spock.lang.Unroll -import java.util.concurrent.ForkJoinPool - /** * This allows the testing of different execution strategies that provide the same results given the same schema, * and queries and results @@ -27,8 +25,7 @@ class ExecutionStrategyEquivalenceTest extends Specification { name } } - """ - : [ + """: [ hero: [ name: 'R2-D2' ] @@ -46,8 +43,7 @@ class ExecutionStrategyEquivalenceTest extends Specification { } } } - """ - : [ + """: [ hero: [ id : '2001', name : 'R2-D2', @@ -73,8 +69,7 @@ class ExecutionStrategyEquivalenceTest extends Specification { name } } - """ - : [ + """: [ human: [ name: 'Luke Skywalker' ] @@ -93,8 +88,7 @@ class ExecutionStrategyEquivalenceTest extends Specification { name } } - """ - : [ + """: [ luke: [ name: 'Luke Skywalker' @@ -132,11 +126,11 @@ class ExecutionStrategyEquivalenceTest extends Specification { where: - strategyType | strategyUnderTest | expectedQueriesAndResults - "async" | new AsyncExecutionStrategy() | standardQueriesAndResults() - "asyncSerial" | new AsyncSerialExecutionStrategy() | standardQueriesAndResults() - "breadthFirst" | new BreadthFirstExecutionTestStrategy() | standardQueriesAndResults() - "breadthFirst" | new BreadthFirstTestStrategy() | standardQueriesAndResults() + strategyType | strategyUnderTest | expectedQueriesAndResults + "async" | new AsyncExecutionStrategy() | standardQueriesAndResults() + "asyncSerial" | new AsyncSerialExecutionStrategy() | standardQueriesAndResults() + "breadthFirst" | new BreadthFirstExecutionTestStrategy() | standardQueriesAndResults() + "breadthFirst" | new BreadthFirstTestStrategy() | standardQueriesAndResults() } From 9232a2889d0dd00c56dfa6bc0ca498afa59a738c Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 18 Feb 2024 20:06:24 +1000 Subject: [PATCH 214/393] tests --- .../graphql/test/StandaloneGraphQLTest.java | 40 +++++++++++++++++++ .../graphql/test/StartAgentOnStartupTest.java | 12 ++++++ agent/build.gradle | 2 +- .../java/graphql/agent/GraphQLJavaAgent.java | 5 +++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 agent-test/src/test/java/graphql/test/StandaloneGraphQLTest.java create mode 100644 agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java diff --git a/agent-test/src/test/java/graphql/test/StandaloneGraphQLTest.java b/agent-test/src/test/java/graphql/test/StandaloneGraphQLTest.java new file mode 100644 index 0000000000..21f845ed5b --- /dev/null +++ b/agent-test/src/test/java/graphql/test/StandaloneGraphQLTest.java @@ -0,0 +1,40 @@ +package graphql.test; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.GraphQLContext; +import graphql.agent.result.ExecutionTrackingResult; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; + +public class StandaloneGraphQLTest { + + public static void main(String[] args) { + String schema = "type Query { hello: String }"; + TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(schema); + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("hello", environment -> "world")) + .build(); + GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("{ hello alias: hello alias2: hello }").build(); + GraphQLContext graphQLContext = executionInput.getGraphQLContext(); + ExecutionResult executionResult = graphQL.execute(executionInput); + System.out.println(executionResult.getData().toString()); + ExecutionTrackingResult executionTrackingResult = graphQLContext.get(ExecutionTrackingResult.EXECUTION_TRACKING_KEY); + if (executionTrackingResult == null) { + System.out.println("No tracking data found"); + System.exit(1); + } + if (executionTrackingResult.timePerPath.size() != 3) { + System.out.println("Expected 3 paths, got " + executionTrackingResult.timePerPath.size()); + System.exit(1); + } + System.out.println("Successfully tracked execution"); + System.exit(0); + } +} diff --git a/agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java b/agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java new file mode 100644 index 0000000000..b19fea43a8 --- /dev/null +++ b/agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java @@ -0,0 +1,12 @@ +package graphql.test; + +import org.junit.jupiter.api.Test; + +public class StartAgentOnStartupTest { + + + @Test + void testAgentCanBeLoadedAtStartup() { + + } +} diff --git a/agent/build.gradle b/agent/build.gradle index 15b46645b9..d312c36276 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -30,9 +30,9 @@ shadowJar { manifest { attributes( 'Agent-Class': 'graphql.agent.GraphQLJavaAgent', + 'Premain-Class': 'graphql.agent.GraphQLJavaAgent', 'Can-Redefine-Classes': 'true', 'Can-Retransform-Classes': 'true', - 'Premain-Class': 'graphql.agent.GraphQLJavaAgent' ) } } diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 8a27cd8a20..16ef07b142 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -37,6 +37,11 @@ public class GraphQLJavaAgent { public static final Map executionIdToData = new ConcurrentHashMap<>(); public static final Map dataLoaderToExecutionId = new ConcurrentHashMap<>(); + public static void premain(String agentArgs, Instrumentation inst) { + agentmain(agentArgs, inst); + } + + public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("GraphQL Java Agent is starting"); new AgentBuilder.Default() From 9d469579b6ebee44a87aa61f7d2d94b65718886d Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 18 Feb 2024 22:31:01 +1100 Subject: [PATCH 215/393] Add more test cases --- src/test/groovy/graphql/parser/ParserTest.groovy | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/test/groovy/graphql/parser/ParserTest.groovy b/src/test/groovy/graphql/parser/ParserTest.groovy index 00b8fef455..d76da7b63a 100644 --- a/src/test/groovy/graphql/parser/ParserTest.groovy +++ b/src/test/groovy/graphql/parser/ParserTest.groovy @@ -1154,8 +1154,6 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" def "escape characters correctly printed when printing AST"() { given: - def src = "\"\\\"\" scalar A" - def env = newParserEnvironment() .document(src) .parserOptions( @@ -1178,9 +1176,17 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" when: def reparsedPrinted = AstPrinter.printAst(reparsed) - + then: reparsedPrinted == printed // Re-parsing and re-printing produces the same result + + where: + src | _ + "\"\\\"\" scalar A" | _ + "\"\f\" scalar A" | _ + "\"\b\" scalar A" | _ + "\"\t\" scalar A" | _ + "\"\\\" scalar A" | _ } } From 7ee3cc5af82f7fd86bfb3bc9f78a59d8dfeaddea Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sun, 18 Feb 2024 22:39:23 +1100 Subject: [PATCH 216/393] Remove invalid schema example --- src/test/groovy/graphql/parser/ParserTest.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/groovy/graphql/parser/ParserTest.groovy b/src/test/groovy/graphql/parser/ParserTest.groovy index d76da7b63a..42e604e9a6 100644 --- a/src/test/groovy/graphql/parser/ParserTest.groovy +++ b/src/test/groovy/graphql/parser/ParserTest.groovy @@ -1186,7 +1186,6 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" "\"\f\" scalar A" | _ "\"\b\" scalar A" | _ "\"\t\" scalar A" | _ - "\"\\\" scalar A" | _ } } From d9d3136af2acf48b2fce7a174185eec809c4d3bb Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 19 Feb 2024 07:46:10 +1100 Subject: [PATCH 217/393] Fix flaky defer test The order of incremental data in the scenario being tested was non-deterministic, so the test needed a few adjustments --- ...eferExecutionSupportIntegrationTest.groovy | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index 46a0756c6a..e7844cc750 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -1,11 +1,11 @@ package graphql.execution.incremental +import com.google.common.collect.Iterables import graphql.Directives import graphql.ExecutionInput import graphql.ExecutionResult import graphql.ExperimentalApi import graphql.GraphQL -import graphql.GraphQLContext import graphql.TestUtil import graphql.execution.pubsub.CapturingSubscriber import graphql.incremental.DelayedIncrementalPartialResult @@ -254,9 +254,9 @@ class DeferExecutionSupportIntegrationTest extends Specification { id2: id } } - ... @defer { + ... @defer(label: "defer-post3") { post3: postById(id: "3") { - ... @defer { + ... @defer(label: "defer-id3") { id3: id } } @@ -277,35 +277,35 @@ class DeferExecutionSupportIntegrationTest extends Specification { def incrementalResults = getIncrementalResults(initialResult) then: - incrementalResults == [ - [ - hasNext : true, - incremental: [ - [ - path: [], - data: [post2: [id2: "2"]] - ] - ] - ], - [ - hasNext : true, - incremental: [ - [ - path: [], - data: [post3: [:]] - ] - ] - ], - [ - hasNext : false, - incremental: [ - [ - path: ["post3"], - data: [id3: "3"] - ] - ] - ] - ] + // Ordering is non-deterministic, so we assert on the things we know are going to be true. + + incrementalResults.size() == 3 + // only the last payload has "hasNext=true" + incrementalResults[0].hasNext == true + incrementalResults[1].hasNext == true + incrementalResults[2].hasNext == false + + // every payload has only 1 incremental item + incrementalResults.every { it.incremental.size() == 1 } + + incrementalResults.any { + it.incremental[0] == [path: [], data: [post2: [id2: "2"]]] + } + + // id3 HAS TO be delivered after post3 + def indexOfPost3 = Iterables.indexOf(incrementalResults, { + it.incremental[0] == [path: [], label: "defer-post3", data: [post3: [:]]] + }) + + def indexOfId3 = Iterables.indexOf(incrementalResults, { + it.incremental[0] == [path: ["post3"], label:"defer-id3", data: [id3: "3"]] + }) + + // Assert that both post3 and id3 are present + indexOfPost3 >= 0 + indexOfId3 >= 0 + // Assert that id3 is delivered after post3 + indexOfId3 > indexOfPost3 } def "defer on interface field"() { From 9e8801c284ea43958d6f060533ee9aa3131693e2 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 19 Feb 2024 11:06:47 +1000 Subject: [PATCH 218/393] test for agent loading at startup --- .../java/graphql/GraphQLApp.java} | 12 ++++++------ .../graphql/test/StartAgentOnStartupTest.java | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) rename agent-test/src/{test/java/graphql/test/StandaloneGraphQLTest.java => main/java/graphql/GraphQLApp.java} (90%) diff --git a/agent-test/src/test/java/graphql/test/StandaloneGraphQLTest.java b/agent-test/src/main/java/graphql/GraphQLApp.java similarity index 90% rename from agent-test/src/test/java/graphql/test/StandaloneGraphQLTest.java rename to agent-test/src/main/java/graphql/GraphQLApp.java index 21f845ed5b..9d12bfdb15 100644 --- a/agent-test/src/test/java/graphql/test/StandaloneGraphQLTest.java +++ b/agent-test/src/main/java/graphql/GraphQLApp.java @@ -1,9 +1,5 @@ -package graphql.test; +package graphql; -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.GraphQL; -import graphql.GraphQLContext; import graphql.agent.result.ExecutionTrackingResult; import graphql.schema.GraphQLSchema; import graphql.schema.idl.RuntimeWiring; @@ -11,7 +7,11 @@ import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; -public class StandaloneGraphQLTest { +/** + * Used for testing loading the agent on startup. + * See StartAgentOnStartupTest + */ +public class GraphQLApp { public static void main(String[] args) { String schema = "type Query { hello: String }"; diff --git a/agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java b/agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java index b19fea43a8..e4c8c3add6 100644 --- a/agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java +++ b/agent-test/src/test/java/graphql/test/StartAgentOnStartupTest.java @@ -2,11 +2,22 @@ import org.junit.jupiter.api.Test; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + public class StartAgentOnStartupTest { @Test - void testAgentCanBeLoadedAtStartup() { - + void testAgentCanBeLoadedAtStartup() throws IOException, InterruptedException { + // we use the classpath of the current test + String classPath = System.getProperty("java.class.path"); + ProcessBuilder processBuilder = new ProcessBuilder("java", "-javaagent:../agent/build/libs/agent.jar", "-classpath", classPath, "graphql.GraphQLApp"); + Process process = processBuilder.start(); + process.getErrorStream().transferTo(System.err); + process.getInputStream().transferTo(System.out); + int i = process.waitFor(); + assertThat(i).isZero(); } } From 2418f5b89672370aa29a49a74b031226be247ac4 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 19 Feb 2024 11:32:48 +1000 Subject: [PATCH 219/393] tests --- .../src/test/java/graphql/test/AgentTest.java | 13 ++++++++++++- .../src/test/java/graphql/test/LoadAgent.java | 2 +- .../agent/result/ExecutionTrackingResult.java | 7 ++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/agent-test/src/test/java/graphql/test/AgentTest.java b/agent-test/src/test/java/graphql/test/AgentTest.java index 8a11911a26..a0add765c2 100644 --- a/agent-test/src/test/java/graphql/test/AgentTest.java +++ b/agent-test/src/test/java/graphql/test/AgentTest.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -13,7 +15,7 @@ public class AgentTest { @BeforeAll static void init() { - LoadAgent.load(); + LoadAgent.loadIntoCurrentJVM(); } @AfterAll @@ -42,6 +44,15 @@ void testBatchLoader() { assertThat(executionTrackingResult.getDfResultTypes("/issues[1]/author")) .isEqualTo(ExecutionTrackingResult.DFResultType.PENDING); + assertThat(executionTrackingResult.getDataLoaderNames()).isEqualTo(Collections.singletonList("userLoader")); + + assertThat(executionTrackingResult.dataLoaderNameToBatchCall).hasSize(1); + List userLoaderCalls = executionTrackingResult.dataLoaderNameToBatchCall.get("userLoader"); + assertThat(userLoaderCalls).hasSize(1); + ExecutionTrackingResult.BatchLoadingCall batchLoadingCall = userLoaderCalls.get(0); + + assertThat(batchLoadingCall.keyCount).isEqualTo(2); + verifyAgentDataIsEmpty(); } diff --git a/agent-test/src/test/java/graphql/test/LoadAgent.java b/agent-test/src/test/java/graphql/test/LoadAgent.java index b549de5239..90ec46a078 100644 --- a/agent-test/src/test/java/graphql/test/LoadAgent.java +++ b/agent-test/src/test/java/graphql/test/LoadAgent.java @@ -8,7 +8,7 @@ public class LoadAgent { - public static void load() { + public static void loadIntoCurrentJVM() { ByteBuddyAgent.attach(new File("../agent/build/libs/agent.jar"), String.valueOf(ProcessHandle.current().pid())); } diff --git a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java index 77c03def75..9896da2c1d 100644 --- a/src/main/java/graphql/agent/result/ExecutionTrackingResult.java +++ b/src/main/java/graphql/agent/result/ExecutionTrackingResult.java @@ -37,6 +37,7 @@ public class ExecutionTrackingResult { public final Map finishedThreadPerPath = new ConcurrentHashMap<>(); public final Map startInvocationThreadPerPath = new ConcurrentHashMap<>(); private final Map dfResultTypes = new ConcurrentHashMap<>(); + public final Map> dataLoaderNameToBatchCall = new ConcurrentHashMap<>(); public static class BatchLoadingCall { public BatchLoadingCall(int keyCount, String threadName) { @@ -46,9 +47,9 @@ public BatchLoadingCall(int keyCount, String threadName) { public final int keyCount; public final String threadName; + } - public final Map> dataLoaderNameToBatchCall = new ConcurrentHashMap<>(); public String print(String executionId) { StringBuilder s = new StringBuilder(); @@ -112,6 +113,10 @@ public enum DFResultType { PENDING, } + public List getDataLoaderNames() { + return new ArrayList<>(dataLoaderToName.values()); + } + public void start(ResultPath path, long startTime) { timePerPath.put(path, startTime); From 86455801f3a391bc8717c0d3624b774cfa53cd14 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 19 Feb 2024 11:33:59 +1000 Subject: [PATCH 220/393] cleanup --- agent/src/main/java/graphql/agent/GraphQLJavaAgent.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index 16ef07b142..a172724671 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -47,8 +47,6 @@ public static void agentmain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() .type(named("graphql.execution.Execution")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // ClassInjector.UsingInstrumentation.of() - // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(ExecutionAdvice.class).on(nameMatches("executeOperation"))); @@ -60,19 +58,16 @@ public static void agentmain(String agentArgs, Instrumentation inst) { }) .type(named("org.dataloader.DataLoaderRegistry")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(DataLoaderRegistryAdvice.class).on(nameMatches("dispatchAll"))); }) .type(named("org.dataloader.DataLoader")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(DataLoaderLoadAdvice.class).on(nameMatches("load"))); }) .type(named("org.dataloader.DataLoaderHelper")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(DataLoaderHelperDispatchAdvice.class).on(nameMatches("dispatch"))) .visit(Advice.to(DataLoaderHelperInvokeBatchLoaderAdvice.class) @@ -80,7 +75,6 @@ public static void agentmain(String agentArgs, Instrumentation inst) { }) .type(named("graphql.schema.DataFetchingEnvironmentImpl")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> { - // System.out.println("transforming " + typeDescription); return builder .visit(Advice.to(DataFetchingEnvironmentAdvice.class).on(nameMatches("getDataLoader"))); }) @@ -267,11 +261,9 @@ public static void getDataLoader(@Advice.Argument(0) String dataLoaderName, @Advice.This(typing = Assigner.Typing.DYNAMIC) DataFetchingEnvironment dataFetchingEnvironment, @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) DataLoader dataLoader) { ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(dataFetchingEnvironment.getExecutionId()); - // System.out.println("execution data: " + ExecutionTrackingResult); ResultPath resultPath = dataFetchingEnvironment.getExecutionStepInfo().getPath(); executionTrackingResult.resultPathToDataLoaderUsed.put(resultPath, dataLoaderName); - // System.out.println(dataLoaderName + " > " + dataLoader); } } @@ -283,7 +275,6 @@ class DataLoaderLoadAdvice { public static void load(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object dataLoader) { ExecutionId executionId = GraphQLJavaAgent.dataLoaderToExecutionId.get(dataLoader); String dataLoaderName = GraphQLJavaAgent.executionIdToData.get(executionId).dataLoaderToName.get(dataLoader); - // System.out.println("dataloader " + dataLoaderName + " load for execution " + executionId); } } From fb54a21915c2459e41392016b97d3c53f42d4884 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 19 Feb 2024 12:49:15 +1100 Subject: [PATCH 221/393] This removes the CompletableFuture signature from InstrumentationContext --- src/main/java/graphql/GraphQL.java | 13 ++++--------- .../execution/AsyncExecutionStrategy.java | 2 +- .../AsyncSerialExecutionStrategy.java | 2 +- .../java/graphql/execution/Execution.java | 4 ++-- .../graphql/execution/ExecutionStrategy.java | 10 +++++----- .../SubscriptionExecutionStrategy.java | 4 ++-- .../ChainedInstrumentation.java | 16 ++++++++-------- .../ExecuteObjectInstrumentationContext.java | 2 +- ...ecutionStrategyInstrumentationContext.java | 2 +- .../InstrumentationContext.java | 2 +- .../SimpleInstrumentationContext.java | 19 +++++++------------ ...teObjectInstrumentationContextAdapter.java | 16 ++++++++-------- ...onResultInstrumentationContextAdapter.java | 15 +++++++-------- .../FieldLevelTrackingApproach.java | 6 +++--- .../AsyncExecutionStrategyTest.groovy | 2 +- .../InstrumentationTest.groovy | 2 +- .../TestingInstrumentContext.groovy | 2 +- 17 files changed, 54 insertions(+), 65 deletions(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 590b252eb8..9baec35103 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -421,17 +421,16 @@ public CompletableFuture executeAsync(ExecutionInput executionI InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInputWithId, this.graphQLSchema, instrumentationState); ExecutionInput instrumentedExecutionInput = instrumentation.instrumentExecutionInput(executionInputWithId, inputInstrumentationParameters, instrumentationState); - CompletableFuture beginExecutionCF = new CompletableFuture<>(); InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(instrumentedExecutionInput, this.graphQLSchema, instrumentationState); InstrumentationContext executionInstrumentation = nonNullCtx(instrumentation.beginExecution(instrumentationParameters, instrumentationState)); - executionInstrumentation.onDispatched(beginExecutionCF); + executionInstrumentation.onDispatched(); GraphQLSchema graphQLSchema = instrumentation.instrumentSchema(this.graphQLSchema, instrumentationParameters, instrumentationState); CompletableFuture executionResult = parseValidateAndExecute(instrumentedExecutionInput, graphQLSchema, instrumentationState); // // finish up instrumentation - executionResult = executionResult.whenComplete(completeInstrumentationCtxCF(executionInstrumentation, beginExecutionCF)); + executionResult = executionResult.whenComplete(completeInstrumentationCtxCF(executionInstrumentation)); // // allow instrumentation to tweak the result executionResult = executionResult.thenCompose(result -> instrumentation.instrumentExecutionResult(result, instrumentationParameters, instrumentationState)); @@ -508,15 +507,13 @@ private PreparsedDocumentEntry parseAndValidate(AtomicReference private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters(executionInput, graphQLSchema, instrumentationState); InstrumentationContext parseInstrumentationCtx = nonNullCtx(instrumentation.beginParse(parameters, instrumentationState)); - CompletableFuture documentCF = new CompletableFuture<>(); - parseInstrumentationCtx.onDispatched(documentCF); + parseInstrumentationCtx.onDispatched(); ParseAndValidateResult parseResult = ParseAndValidate.parse(executionInput); if (parseResult.isFailure()) { parseInstrumentationCtx.onCompleted(null, parseResult.getSyntaxException()); return parseResult; } else { - documentCF.complete(parseResult.getDocument()); parseInstrumentationCtx.onCompleted(parseResult.getDocument(), null); DocumentAndVariables documentAndVariables = parseResult.getDocumentAndVariables(); @@ -528,15 +525,13 @@ private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchem private List validate(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema, instrumentationState), instrumentationState)); - CompletableFuture> cf = new CompletableFuture<>(); - validationCtx.onDispatched(cf); + validationCtx.onDispatched(); Predicate> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true); Locale locale = executionInput.getLocale() != null ? executionInput.getLocale() : Locale.getDefault(); List validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate, locale); validationCtx.onCompleted(validationErrors, null); - cf.complete(validationErrors); return validationErrors; } diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 96419730cf..fa1d191440 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -48,7 +48,7 @@ public CompletableFuture execute(ExecutionContext executionCont Async.CombinedBuilder futures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport); CompletableFuture overallResult = new CompletableFuture<>(); - executionStrategyCtx.onDispatched(overallResult); + executionStrategyCtx.onDispatched(); futures.await().whenComplete((completeValueInfos, throwable) -> { List fieldsExecutedOnInitialResult = deferredExecutionSupport.getNonDeferredFieldNames(fieldNames); diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index 1c0e4b3924..baba51595c 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -48,7 +48,7 @@ public CompletableFuture execute(ExecutionContext executionCont }); CompletableFuture overallResult = new CompletableFuture<>(); - executionStrategyCtx.onDispatched(overallResult); + executionStrategyCtx.onDispatched(); resultsFuture.whenComplete(handleResults(executionContext, fieldNames, overallResult)); overallResult.whenComplete(executionStrategyCtx::onCompleted); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 6199e0b3f0..3c9319770b 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -124,7 +124,7 @@ private CompletableFuture executeOperation(ExecutionContext exe ExecutionResult executionResult = new ExecutionResultImpl(Collections.singletonList((GraphQLError) rte)); CompletableFuture resultCompletableFuture = completedFuture(executionResult); - executeOperationCtx.onDispatched(resultCompletableFuture); + executeOperationCtx.onDispatched(); executeOperationCtx.onCompleted(executionResult, rte); return resultCompletableFuture; } @@ -176,7 +176,7 @@ private CompletableFuture executeOperation(ExecutionContext exe } // note this happens NOW - not when the result completes - executeOperationCtx.onDispatched(result); + executeOperationCtx.onDispatched(); // fill out extensions if we have them result = result.thenApply(er -> mergeExtensionsBuilderIfPresent(er, graphQLContext)); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 0a92931226..b3756a99f2 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -206,7 +206,7 @@ protected CompletableFuture> executeObject(ExecutionContext Async.CombinedBuilder resolvedFieldFutures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport); CompletableFuture> overallResult = new CompletableFuture<>(); - resolveObjectCtx.onDispatched(overallResult); + resolveObjectCtx.onDispatched(); resolvedFieldFutures.await().whenComplete((completeValueInfos, throwable) -> { List fieldsExecutedOnInitialResult = deferredExecutionSupport.getNonDeferredFieldNames(fieldNames); @@ -347,7 +347,7 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); - fieldCtx.onDispatched(fieldValueFuture); + fieldCtx.onDispatched(); fieldValueFuture.whenComplete(fieldCtx::onCompleted); return result; } @@ -417,7 +417,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); - fetchCtx.onDispatched(fetchedValue); + fetchCtx.onDispatched(); return fetchedValue .handle((result, exception) -> { fetchCtx.onCompleted(result, exception); @@ -567,7 +567,7 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters); CompletableFuture executionResultFuture = fieldValueInfo.getFieldValueFuture(); - ctxCompleteField.onDispatched(executionResultFuture); + ctxCompleteField.onDispatched(); executionResultFuture.whenComplete(ctxCompleteField::onCompleted); return fieldValueInfo; } @@ -721,7 +721,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, CompletableFuture> resultsFuture = Async.each(fieldValueInfos, FieldValueInfo::getFieldValueFuture); CompletableFuture overallResult = new CompletableFuture<>(); - completeListCtx.onDispatched(overallResult); + completeListCtx.onDispatched(); overallResult.whenComplete(completeListCtx::onCompleted); resultsFuture.whenComplete((results, exception) -> { diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index ca0c7f98ae..f735b0dc6a 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -69,7 +69,7 @@ public CompletableFuture execute(ExecutionContext executionCont }); // dispatched the subscription query - executionStrategyCtx.onDispatched(overallResult); + executionStrategyCtx.onDispatched(); overallResult.whenComplete(executionStrategyCtx::onCompleted); return overallResult; @@ -140,7 +140,7 @@ private CompletableFuture executeSubscriptionEvent(ExecutionCon .thenApply(executionResult -> wrapWithRootFieldName(newParameters, executionResult)); // dispatch instrumentation so they can know about each subscription event - subscribedFieldCtx.onDispatched(overallResult); + subscribedFieldCtx.onDispatched(); overallResult.whenComplete(subscribedFieldCtx::onCompleted); // allow them to instrument each ER should they want to diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 35847dfdde..8b61fb72f7 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -443,8 +443,8 @@ private static class ChainedInstrumentationContext implements Instrumentation } @Override - public void onDispatched(CompletableFuture result) { - contexts.forEach(context -> context.onDispatched(result)); + public void onDispatched() { + contexts.forEach(InstrumentationContext::onDispatched); } @Override @@ -462,8 +462,8 @@ private static class ChainedExecutionStrategyInstrumentationContext implements E } @Override - public void onDispatched(CompletableFuture result) { - contexts.forEach(context -> context.onDispatched(result)); + public void onDispatched() { + contexts.forEach(InstrumentationContext::onDispatched); } @Override @@ -491,8 +491,8 @@ private static class ChainedExecuteObjectInstrumentationContext implements Execu } @Override - public void onDispatched(CompletableFuture> result) { - contexts.forEach(context -> context.onDispatched(result)); + public void onDispatched() { + contexts.forEach(InstrumentationContext::onDispatched); } @Override @@ -520,8 +520,8 @@ private static class ChainedDeferredExecutionStrategyInstrumentationContext impl } @Override - public void onDispatched(CompletableFuture result) { - contexts.forEach(context -> context.onDispatched(result)); + public void onDispatched() { + contexts.forEach(InstrumentationContext::onDispatched); } @Override diff --git a/src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java index f78cbdc0b0..4e100238df 100644 --- a/src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/ExecuteObjectInstrumentationContext.java @@ -15,7 +15,7 @@ public interface ExecuteObjectInstrumentationContext extends InstrumentationCont @Internal ExecuteObjectInstrumentationContext NOOP = new ExecuteObjectInstrumentationContext() { @Override - public void onDispatched(CompletableFuture> result) { + public void onDispatched() { } @Override diff --git a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java index 04fbceab81..7fc0a3e0d3 100644 --- a/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/ExecutionStrategyInstrumentationContext.java @@ -36,7 +36,7 @@ static ExecutionStrategyInstrumentationContext nonNullCtx(ExecutionStrategyInstr @Internal ExecutionStrategyInstrumentationContext NOOP = new ExecutionStrategyInstrumentationContext() { @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched() { } @Override diff --git a/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java index 2d9626a113..83e557116c 100644 --- a/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java @@ -20,7 +20,7 @@ public interface InstrumentationContext { * * @param result the result of the step as a completable future */ - void onDispatched(CompletableFuture result); + void onDispatched(); /** * This is invoked when the instrumentation step is fully completed diff --git a/src/main/java/graphql/execution/instrumentation/SimpleInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/SimpleInstrumentationContext.java index 2621314a56..68c70b214e 100644 --- a/src/main/java/graphql/execution/instrumentation/SimpleInstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/SimpleInstrumentationContext.java @@ -15,7 +15,7 @@ public class SimpleInstrumentationContext implements InstrumentationContext NO_OP = new InstrumentationContext() { @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched() { } @Override @@ -49,21 +49,21 @@ public static InstrumentationContext nonNullCtx(InstrumentationContext } private final BiConsumer codeToRunOnComplete; - private final Consumer> codeToRunOnDispatch; + private final Runnable codeToRunOnDispatch; public SimpleInstrumentationContext() { this(null, null); } - private SimpleInstrumentationContext(Consumer> codeToRunOnDispatch, BiConsumer codeToRunOnComplete) { + private SimpleInstrumentationContext(Runnable codeToRunOnDispatch, BiConsumer codeToRunOnComplete) { this.codeToRunOnComplete = codeToRunOnComplete; this.codeToRunOnDispatch = codeToRunOnDispatch; } @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched() { if (codeToRunOnDispatch != null) { - codeToRunOnDispatch.accept(result); + codeToRunOnDispatch.run(); } } @@ -83,7 +83,7 @@ public void onCompleted(T result, Throwable t) { * * @return an instrumentation context */ - public static SimpleInstrumentationContext whenDispatched(Consumer> codeToRun) { + public static SimpleInstrumentationContext whenDispatched(Runnable codeToRun) { return new SimpleInstrumentationContext<>(codeToRun, null); } @@ -101,13 +101,8 @@ public static SimpleInstrumentationContext whenCompleted(BiConsumer BiConsumer completeInstrumentationCtxCF( - InstrumentationContext instrumentationContext, CompletableFuture targetCF) { + InstrumentationContext instrumentationContext) { return (result, throwable) -> { - if (throwable != null) { - targetCF.completeExceptionally(throwable); - } else { - targetCF.complete(result); - } nonNullCtx(instrumentationContext).onCompleted(result, throwable); }; } diff --git a/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java b/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java index 2c42d55906..2fd133eb89 100644 --- a/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java +++ b/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java @@ -4,12 +4,11 @@ import graphql.ExecutionResultImpl; import graphql.Internal; import graphql.execution.FieldValueInfo; -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; +import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; /** * A class to help adapt old {@link ExecutionResult} based ExecutionStrategyInstrumentationContext @@ -25,16 +24,17 @@ public ExecuteObjectInstrumentationContextAdapter(ExecutionStrategyInstrumentati } @Override - public void onDispatched(CompletableFuture> result) { - CompletableFuture future = result.thenApply(r -> ExecutionResultImpl.newExecutionResult().data(r).build()); - delegate.onDispatched(future); - // - // when the mapped future is completed, then call onCompleted on the delegate - future.whenComplete(delegate::onCompleted); + public void onDispatched() { + delegate.onDispatched(); } @Override public void onCompleted(Map result, Throwable t) { + if (t != null) { + delegate.onCompleted(null, t); + } else { + delegate.onCompleted(ExecutionResultImpl.newExecutionResult().data(result).build(), null); + } } @Override diff --git a/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java b/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java index d67eddb8d0..063bc75909 100644 --- a/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java +++ b/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java @@ -5,8 +5,6 @@ import graphql.Internal; import graphql.execution.instrumentation.InstrumentationContext; -import java.util.concurrent.CompletableFuture; - /** * A class to help adapt old {@link ExecutionResult} based InstrumentationContext * from the newer {@link Object} based ones. @@ -21,15 +19,16 @@ public ExecutionResultInstrumentationContextAdapter(InstrumentationContext result) { - CompletableFuture future = result.thenApply(obj -> ExecutionResultImpl.newExecutionResult().data(obj).build()); - delegate.onDispatched(future); - // - // when the mapped future is completed, then call onCompleted on the delegate - future.whenComplete(delegate::onCompleted); + public void onDispatched() { + delegate.onDispatched(); } @Override public void onCompleted(Object result, Throwable t) { + if (t != null) { + delegate.onCompleted(null, t); + } else { + delegate.onCompleted(ExecutionResultImpl.newExecutionResult().data(result).build(), null); + } } } diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index fe1d865277..c0e62796fd 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -123,7 +123,7 @@ ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationEx return new ExecutionStrategyInstrumentationContext() { @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched() { } @@ -154,7 +154,7 @@ ExecuteObjectInstrumentationContext beginObjectResolution(InstrumentationExecuti return new ExecuteObjectInstrumentationContext() { @Override - public void onDispatched(CompletableFuture> result) { + public void onDispatched() { } @Override @@ -229,7 +229,7 @@ public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchP return new InstrumentationContext() { @Override - public void onDispatched(CompletableFuture result) { + public void onDispatched() { boolean dispatchNeeded; synchronized (callStack) { callStack.increaseFetchCount(level); diff --git a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy index 2c9bbf5263..8769bb79a4 100644 --- a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy @@ -287,7 +287,7 @@ abstract class AsyncExecutionStrategyTest extends Specification { } @Override - void onDispatched(CompletableFuture result) { + void onDispatched() { } @Override diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index 9da4e4990c..9e1d800200 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -171,7 +171,7 @@ class InstrumentationTest extends Specification { return new ExecutionStrategyInstrumentationContext() { @Override - void onDispatched(CompletableFuture result) { + void onDispatched() { System.out.println(String.format("t%s setting go signal on", Thread.currentThread().getId())) goSignal.set(true) } diff --git a/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy b/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy index cdfaa84513..402fd2aee0 100644 --- a/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy @@ -25,7 +25,7 @@ class TestingInstrumentContext implements InstrumentationContext { } @Override - void onDispatched(CompletableFuture result) { + void onDispatched() { if (useOnDispatch) { this.executionList << "onDispatched:$op" } From 48a7351071790d86837db844da34391e21f938bd Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 19 Feb 2024 12:59:53 +1100 Subject: [PATCH 222/393] This removes the CompletableFuture signature from InstrumentationContext - javadoc --- .../execution/instrumentation/InstrumentationContext.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java index 83e557116c..2bb52272f8 100644 --- a/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java +++ b/src/main/java/graphql/execution/instrumentation/InstrumentationContext.java @@ -17,8 +17,6 @@ public interface InstrumentationContext { /** * This is invoked when the instrumentation step is initially dispatched - * - * @param result the result of the step as a completable future */ void onDispatched(); From bcf4174a98c032ff2061cb62015e0d407a0ea349 Mon Sep 17 00:00:00 2001 From: Danny Thomas Date: Mon, 19 Feb 2024 17:28:54 +1100 Subject: [PATCH 223/393] Avoid Map for instrumentation state. Fixes #3436 Signed-off-by: Danny Thomas --- .../ChainedInstrumentation.java | 213 +++++++----------- .../NoContextChainedInstrumentation.java | 5 +- 2 files changed, 88 insertions(+), 130 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 35847dfdde..b1b5eb2dbb 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -1,7 +1,6 @@ package graphql.execution.instrumentation; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Maps; import graphql.Assert; import graphql.ExecutionInput; import graphql.ExecutionResult; @@ -25,16 +24,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.AbstractMap; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; import static graphql.Assert.assertNotNull; -import static graphql.collect.ImmutableKit.mapAndDropNulls; /** * This allows you to chain together a number of {@link graphql.execution.instrumentation.Instrumentation} implementations @@ -67,21 +66,48 @@ public List getInstrumentations() { return instrumentations; } - protected InstrumentationState getSpecificState(Instrumentation instrumentation, InstrumentationState parametersInstrumentationState) { - ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) parametersInstrumentationState; - return chainedInstrumentationState.getState(instrumentation); - } - - private InstrumentationContext chainedCtx(Function> mapper) { + private InstrumentationContext chainedCtx(InstrumentationState state, BiFunction> mapper) { // if we have zero or 1 instrumentations (and 1 is the most common), then we can avoid an object allocation // of the ChainedInstrumentationContext since it won't be needed if (instrumentations.isEmpty()) { return SimpleInstrumentationContext.noOp(); } + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; if (instrumentations.size() == 1) { - return mapper.apply(instrumentations.get(0)); + return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0)); + } + return new ChainedInstrumentationContext<>(chainedMapAndDropNulls(chainedInstrumentationState, mapper)); + } + + private T chainedInstrument(InstrumentationState state, T input, ChainedInstrumentationFunction mapper) { + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; + for (int i = 0; i < instrumentations.size(); i++) { + Instrumentation instrumentation = instrumentations.get(i); + InstrumentationState specificState = chainedInstrumentationState.getState(i); + input = mapper.apply(instrumentation, specificState, input); + } + return input; + } + + protected ImmutableList chainedMapAndDropNulls(InstrumentationState state, BiFunction mapper) { + ImmutableList.Builder result = ImmutableList.builderWithExpectedSize(instrumentations.size()); + for (int i = 0; i < instrumentations.size(); i++) { + Instrumentation instrumentation = instrumentations.get(i); + InstrumentationState specificState = ((ChainedInstrumentationState) state).getState(i); + T value = mapper.apply(instrumentation, specificState); + if (value != null) { + result.add(value); + } + } + return result.build(); + } + + protected void chainedConsume(InstrumentationState state, BiConsumer stateConsumer) { + for (int i = 0; i < instrumentations.size(); i++) { + Instrumentation instrumentation = instrumentations.get(i); + InstrumentationState specificState = ((ChainedInstrumentationState) state).getState(i); + stateConsumer.accept(instrumentation, specificState); } - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, mapper)); } @Override @@ -109,10 +135,7 @@ public InstrumentationContext beginExecution(InstrumentationExe @Override public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginExecution(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginExecution(parameters, specificState)); } @Override @@ -123,10 +146,7 @@ public InstrumentationContext beginParse(InstrumentationExecutionParam @Override public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginParse(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginParse(parameters, specificState)); } @Override @@ -137,10 +157,7 @@ public InstrumentationContext> beginValidation(Instrumenta @Override public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginValidation(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginValidation(parameters, specificState)); } @Override @@ -151,10 +168,7 @@ public InstrumentationContext beginExecuteOperation(Instrumenta @Override public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginExecuteOperation(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginExecuteOperation(parameters, specificState)); } @Override @@ -168,14 +182,12 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument if (instrumentations.isEmpty()) { return ExecutionStrategyInstrumentationContext.NOOP; } - Function mapper = instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginExecutionStrategy(parameters, specificState); - }; + BiFunction mapper = (instrumentation, specificState) -> instrumentation.beginExecutionStrategy(parameters, specificState); + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; if (instrumentations.size() == 1) { - return mapper.apply(instrumentations.get(0)); + return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0)); } - return new ChainedExecutionStrategyInstrumentationContext(mapAndDropNulls(instrumentations, mapper)); + return new ChainedExecutionStrategyInstrumentationContext(chainedMapAndDropNulls(chainedInstrumentationState, mapper)); } @Override @@ -183,25 +195,18 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument if (instrumentations.isEmpty()) { return ExecuteObjectInstrumentationContext.NOOP; } - Function mapper = instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginExecuteObject(parameters, specificState); - }; - if (instrumentations.size() == 1) { - return mapper.apply(instrumentations.get(0)); + BiFunction mapper = (instrumentation, specificState) -> instrumentation.beginExecuteObject(parameters, specificState); + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; + if (instrumentations.size() == 1) { + return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0)); } - return new ChainedExecuteObjectInstrumentationContext(mapAndDropNulls(instrumentations, mapper)); + return new ChainedExecuteObjectInstrumentationContext(chainedMapAndDropNulls(chainedInstrumentationState, mapper)); } @ExperimentalApi @Override public InstrumentationContext beginDeferredField(InstrumentationState instrumentationState) { - return new ChainedDeferredExecutionStrategyInstrumentationContext(instrumentations.stream() - .map(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, instrumentationState); - return instrumentation.beginDeferredField(specificState); - }) - .collect(Collectors.toList())); + return new ChainedDeferredExecutionStrategyInstrumentationContext(chainedMapAndDropNulls(instrumentationState, Instrumentation::beginDeferredField)); } @Override @@ -212,10 +217,7 @@ public InstrumentationContext beginSubscribedFieldEvent(Instrum @Override public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginSubscribedFieldEvent(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginSubscribedFieldEvent(parameters, specificState)); } @Override @@ -231,10 +233,7 @@ public InstrumentationContext beginField(InstrumentationFieldPa @Override public @Nullable InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginFieldExecution(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldExecution(parameters, specificState)); } @Override @@ -245,10 +244,7 @@ public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchP @Override public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginFieldFetch(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState)); } @@ -265,10 +261,7 @@ public InstrumentationContext beginFieldComplete(Instrumentatio @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginFieldCompletion(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldCompletion(parameters, specificState)); } @@ -280,18 +273,12 @@ public InstrumentationContext beginFieldListComplete(Instrument @Override public InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginFieldListComplete(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldListComplete(parameters, specificState)); } @Override public @Nullable InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginFieldListCompletion(parameters, specificState); - }); + return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldListCompletion(parameters, specificState)); } @Override @@ -303,14 +290,7 @@ public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, In @NotNull @Override public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { - if (instrumentations.isEmpty()) { - return executionInput; - } - for (Instrumentation instrumentation : instrumentations) { - InstrumentationState specificState = getSpecificState(instrumentation, state); - executionInput = instrumentation.instrumentExecutionInput(executionInput, parameters, specificState); - } - return executionInput; + return chainedInstrument(state, executionInput, (instrumentation, specificState, accumulator) -> instrumentation.instrumentExecutionInput(accumulator, parameters, specificState)); } @Override @@ -322,14 +302,8 @@ public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables @NotNull @Override public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters, InstrumentationState state) { - if (instrumentations.isEmpty()) { - return documentAndVariables; - } - for (Instrumentation instrumentation : instrumentations) { - InstrumentationState specificState = getSpecificState(instrumentation, state); - documentAndVariables = instrumentation.instrumentDocumentAndVariables(documentAndVariables, parameters, specificState); - } - return documentAndVariables; + return chainedInstrument(state, documentAndVariables, (instrumentation, specificState, accumulator) -> + instrumentation.instrumentDocumentAndVariables(accumulator, parameters, specificState)); } @Override @@ -341,14 +315,8 @@ public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecu @NotNull @Override public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) { - if (instrumentations.isEmpty()) { - return schema; - } - for (Instrumentation instrumentation : instrumentations) { - InstrumentationState specificState = getSpecificState(instrumentation, state); - schema = instrumentation.instrumentSchema(schema, parameters, specificState); - } - return schema; + return chainedInstrument(state, schema, (instrumentation, specificState, accumulator) -> + instrumentation.instrumentSchema(accumulator, parameters, specificState)); } @Override @@ -360,14 +328,8 @@ public ExecutionContext instrumentExecutionContext(ExecutionContext executionCon @NotNull @Override public ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { - if (instrumentations.isEmpty()) { - return executionContext; - } - for (Instrumentation instrumentation : instrumentations) { - InstrumentationState specificState = getSpecificState(instrumentation, state); - executionContext = instrumentation.instrumentExecutionContext(executionContext, parameters, specificState); - } - return executionContext; + return chainedInstrument(state, executionContext, (instrumentation, specificState, accumulator) -> + instrumentation.instrumentExecutionContext(accumulator, parameters, specificState)); } @Override @@ -379,14 +341,8 @@ public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrume @NotNull @Override public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { - if (instrumentations.isEmpty()) { - return dataFetcher; - } - for (Instrumentation instrumentation : instrumentations) { - InstrumentationState specificState = getSpecificState(instrumentation, state); - dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, parameters, specificState); - } - return dataFetcher; + return chainedInstrument(state, dataFetcher, (Instrumentation instrumentation, InstrumentationState specificState, DataFetcher accumulator) -> + instrumentation.instrumentDataFetcher(accumulator, parameters, specificState)); } @Override @@ -398,8 +354,10 @@ public CompletableFuture instrumentExecutionResult(ExecutionRes @NotNull @Override public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { - CompletableFuture> resultsFuture = Async.eachSequentially(instrumentations, (instrumentation, prevResults) -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); + ImmutableList> entries = chainedMapAndDropNulls(state, AbstractMap.SimpleEntry::new); + CompletableFuture> resultsFuture = Async.eachSequentially(entries, (entry, prevResults) -> { + Instrumentation instrumentation = entry.getKey(); + InstrumentationState specificState = entry.getValue(); ExecutionResult lastResult = prevResults.size() > 0 ? prevResults.get(prevResults.size() - 1) : executionResult; return instrumentation.instrumentExecutionResult(lastResult, parameters, specificState); }); @@ -407,16 +365,14 @@ public CompletableFuture instrumentExecutionResult(ExecutionRes } static class ChainedInstrumentationState implements InstrumentationState { - private final Map instrumentationToStates; + private final List instrumentationStates; + private ChainedInstrumentationState(List instrumentationStates) { + this.instrumentationStates = instrumentationStates; + } - private ChainedInstrumentationState(List instrumentations, List instrumentationStates) { - instrumentationToStates = Maps.newLinkedHashMapWithExpectedSize(instrumentations.size()); - for (int i = 0; i < instrumentations.size(); i++) { - Instrumentation instrumentation = instrumentations.get(i); - InstrumentationState instrumentationState = instrumentationStates.get(i); - instrumentationToStates.put(instrumentation, instrumentationState); - } + private InstrumentationState getState(int index) { + return instrumentationStates.get(index); } private static CompletableFuture combineAll(List instrumentations, InstrumentationCreateStateParameters parameters) { @@ -426,11 +382,7 @@ private static CompletableFuture combineAll(List stateCF = Async.orNullCompletedFuture(instrumentation.createStateAsync(parameters)); builder.add(stateCF); } - return builder.await().thenApply(instrumentationStates -> new ChainedInstrumentationState(instrumentations, instrumentationStates)); - } - - private InstrumentationState getState(Instrumentation instrumentation) { - return instrumentationToStates.get(instrumentation); + return builder.await().thenApply(ChainedInstrumentationState::new); } } @@ -529,5 +481,14 @@ public void onCompleted(Object result, Throwable t) { contexts.forEach(context -> context.onCompleted(result, t)); } } + + @FunctionalInterface + private interface ChainedInstrumentationFunction { + + R apply(I t, S u, A v); + + } + + } diff --git a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java index 1041e9feb6..1928df84f0 100644 --- a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java @@ -49,10 +49,7 @@ public NoContextChainedInstrumentation(Instrumentation... instrumentations) { } private T runAll(InstrumentationState state, BiConsumer stateConsumer) { - for (Instrumentation instrumentation : instrumentations) { - InstrumentationState specificState = getSpecificState(instrumentation, state); - stateConsumer.accept(instrumentation, specificState); - } + chainedConsume(state, stateConsumer); return null; } From b0b3b6d955243b739cb028ef01137ed2e1997310 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 19 Feb 2024 20:23:24 +1100 Subject: [PATCH 224/393] Removed the deprecated Instrumentation methods --- src/main/java/graphql/GraphQL.java | 10 +- .../MaxQueryComplexityInstrumentation.java | 6 +- .../java/graphql/execution/Execution.java | 2 +- .../SubscriptionExecutionStrategy.java | 2 +- .../ChainedInstrumentation.java | 124 ------ .../instrumentation/Instrumentation.java | 396 ++---------------- .../SimplePerformantInstrumentation.java | 107 +---- ...teObjectInstrumentationContextAdapter.java | 49 --- ...onResultInstrumentationContextAdapter.java | 35 -- .../DataLoaderDispatcherInstrumentation.java | 4 +- ...rumentationExecuteOperationParameters.java | 35 -- .../InstrumentationExecutionParameters.java | 33 +- ...umentationExecutionStrategyParameters.java | 37 -- ...nstrumentationFieldCompleteParameters.java | 36 -- .../InstrumentationFieldFetchParameters.java | 25 -- .../InstrumentationFieldParameters.java | 40 -- .../InstrumentationValidationParameters.java | 20 +- .../tracing/TracingInstrumentation.java | 4 +- src/test/groovy/graphql/GraphQLTest.groovy | 4 +- ...xQueryComplexityInstrumentationTest.groovy | 4 +- .../MaxQueryDepthInstrumentationTest.groovy | 2 +- .../SubscriptionExecutionStrategyTest.groovy | 3 +- .../AllNullTestingInstrumentation.groovy | 12 +- .../InstrumentationTest.groovy | 3 +- .../LegacyTestingInstrumentation.groovy | 65 +-- .../ModernTestingInstrumentation.groovy | 22 +- .../NamedInstrumentation.groovy | 11 +- .../FieldValidationTest.groovy | 2 +- .../readme/InstrumentationExamples.java | 5 +- 29 files changed, 111 insertions(+), 987 deletions(-) delete mode 100644 src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java delete mode 100644 src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 590b252eb8..71ddc9e417 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -418,11 +418,11 @@ public CompletableFuture executeAsync(ExecutionInput executionI CompletableFuture instrumentationStateCF = instrumentation.createStateAsync(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInputWithId)); return Async.orNullCompletedFuture(instrumentationStateCF).thenCompose(instrumentationState -> { try { - InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInputWithId, this.graphQLSchema, instrumentationState); + InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInputWithId, this.graphQLSchema); ExecutionInput instrumentedExecutionInput = instrumentation.instrumentExecutionInput(executionInputWithId, inputInstrumentationParameters, instrumentationState); CompletableFuture beginExecutionCF = new CompletableFuture<>(); - InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(instrumentedExecutionInput, this.graphQLSchema, instrumentationState); + InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(instrumentedExecutionInput, this.graphQLSchema); InstrumentationContext executionInstrumentation = nonNullCtx(instrumentation.beginExecution(instrumentationParameters, instrumentationState)); executionInstrumentation.onDispatched(beginExecutionCF); @@ -444,7 +444,7 @@ public CompletableFuture executeAsync(ExecutionInput executionI private CompletableFuture handleAbortException(ExecutionInput executionInput, InstrumentationState instrumentationState, AbortExecutionException abortException) { CompletableFuture executionResult = CompletableFuture.completedFuture(abortException.toExecutionResult()); - InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(executionInput, this.graphQLSchema, instrumentationState); + InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(executionInput, this.graphQLSchema); // // allow instrumentation to tweak the result executionResult = executionResult.thenCompose(result -> instrumentation.instrumentExecutionResult(result, instrumentationParameters, instrumentationState)); @@ -506,7 +506,7 @@ private PreparsedDocumentEntry parseAndValidate(AtomicReference } private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters(executionInput, graphQLSchema, instrumentationState); + InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters(executionInput, graphQLSchema); InstrumentationContext parseInstrumentationCtx = nonNullCtx(instrumentation.beginParse(parameters, instrumentationState)); CompletableFuture documentCF = new CompletableFuture<>(); parseInstrumentationCtx.onDispatched(documentCF); @@ -527,7 +527,7 @@ private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchem } private List validate(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema, instrumentationState), instrumentationState)); + InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema), instrumentationState)); CompletableFuture> cf = new CompletableFuture<>(); validationCtx.onDispatched(cf); diff --git a/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java b/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java index b952214948..6e47e92bb0 100644 --- a/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java +++ b/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java @@ -14,6 +14,7 @@ import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -78,11 +79,10 @@ public MaxQueryComplexityInstrumentation(int maxComplexity, FieldComplexityCalcu } @Override - public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return new State(); + public @Nullable CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + return CompletableFuture.completedFuture(new State()); } - @Override public @Nullable InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState rawState) { State state = ofState(rawState); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 6199e0b3f0..fb1f7856fb 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -99,7 +99,7 @@ public CompletableFuture execute(Document document, GraphQLSche InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters( - executionInput, graphQLSchema, instrumentationState + executionInput, graphQLSchema ); executionContext = instrumentation.instrumentExecutionContext(executionContext, parameters, instrumentationState); return executeOperation(executionContext, executionInput.getRoot(), executionContext.getOperationDefinition()); diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index ca0c7f98ae..899f85abef 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -145,7 +145,7 @@ private CompletableFuture executeSubscriptionEvent(ExecutionCon // allow them to instrument each ER should they want to InstrumentationExecutionParameters i13nExecutionParameters = new InstrumentationExecutionParameters( - executionContext.getExecutionInput(), executionContext.getGraphQLSchema(), executionContext.getInstrumentationState()); + executionContext.getExecutionInput(), executionContext.getGraphQLSchema()); overallResult = overallResult.thenCompose(executionResult -> instrumentation.instrumentExecutionResult(executionResult, i13nExecutionParameters, executionContext.getInstrumentationState())); return overallResult; diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 35847dfdde..900466933f 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -84,29 +84,11 @@ private InstrumentationContext chainedCtx(Function(mapAndDropNulls(instrumentations, mapper)); } - @Override - public InstrumentationState createState() { - return Assert.assertShouldNeverHappen("createStateAsync should only ever be used"); - } - - @Override - public @Nullable InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return Assert.assertShouldNeverHappen("createStateAsync should only ever be used"); - } - @Override public @NotNull CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { return ChainedInstrumentationState.combineAll(instrumentations, parameters); } - @Override - @NotNull - public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { - // these assert methods have been left in so that we truly never call these methods, either in production nor in tests - // later when the deprecated methods are removed, this will disappear. - return Assert.assertShouldNeverHappen("The deprecated " + "beginExecution" + " was called"); - } - @Override public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { @@ -115,11 +97,6 @@ public InstrumentationContext beginExecution(InstrumentationExe }); } - @Override - @NotNull - public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginParse" + " was called"); - } @Override public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { @@ -129,11 +106,6 @@ public InstrumentationContext beginParse(InstrumentationExecutionParam }); } - @Override - @NotNull - public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginValidation" + " was called"); - } @Override public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { @@ -143,12 +115,6 @@ public InstrumentationContext> beginValidation(Instrumenta }); } - @Override - @NotNull - public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginExecuteOperation" + " was called"); - } - @Override public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { @@ -157,11 +123,6 @@ public InstrumentationContext beginExecuteOperation(Instrumenta }); } - @Override - @NotNull - public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginExecutionStrategy" + " was called"); - } @Override public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { @@ -204,11 +165,6 @@ public InstrumentationContext beginDeferredField(InstrumentationState in .collect(Collectors.toList())); } - @Override - @NotNull - public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginSubscribedFieldEvent" + " was called"); - } @Override public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { @@ -218,17 +174,6 @@ public InstrumentationContext beginSubscribedFieldEvent(Instrum }); } - @Override - @NotNull - public InstrumentationContext beginField(InstrumentationFieldParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginField" + " was called"); - } - - @Override - public InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginField" + " was called"); - } - @Override public @Nullable InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { @@ -237,12 +182,6 @@ public InstrumentationContext beginField(InstrumentationFieldPa }); } - @Override - @NotNull - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldFetch" + " was called"); - } - @Override public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { @@ -251,18 +190,6 @@ public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchP }); } - - @Override - @NotNull - public InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldComplete" + " was called"); - } - - @Override - public InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldComplete" + " was called"); - } - @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { @@ -271,21 +198,6 @@ public InstrumentationContext beginFieldComplete(Instrumentatio }); } - - @Override - @NotNull - public InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldListComplete" + " was called"); - } - - @Override - public InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return chainedCtx(instrumentation -> { - InstrumentationState specificState = getSpecificState(instrumentation, state); - return instrumentation.beginFieldListComplete(parameters, specificState); - }); - } - @Override public @Nullable InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return chainedCtx(instrumentation -> { @@ -294,12 +206,6 @@ public InstrumentationContext beginFieldListComplete(Instrument }); } - @Override - @NotNull - public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "instrumentExecutionInput" + " was called"); - } - @NotNull @Override public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { @@ -313,12 +219,6 @@ public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, In return executionInput; } - @Override - @NotNull - public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "instrumentDocumentAndVariables" + " was called"); - } - @NotNull @Override public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters, InstrumentationState state) { @@ -332,12 +232,6 @@ public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables return documentAndVariables; } - @Override - @NotNull - public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "instrumentSchema" + " was called"); - } - @NotNull @Override public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) { @@ -351,12 +245,6 @@ public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecu return schema; } - @Override - @NotNull - public ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "instrumentExecutionContext" + " was called"); - } - @NotNull @Override public ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { @@ -370,12 +258,6 @@ public ExecutionContext instrumentExecutionContext(ExecutionContext executionCon return executionContext; } - @Override - @NotNull - public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "instrumentDataFetcher" + " was called"); - } - @NotNull @Override public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { @@ -389,12 +271,6 @@ public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrume return dataFetcher; } - @Override - @NotNull - public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { - return Assert.assertShouldNeverHappen("The deprecated " + "instrumentExecutionResult" + " was called"); - } - @NotNull @Override public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index bce13bbd34..ef3f12e84e 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -5,8 +5,6 @@ import graphql.ExperimentalApi; import graphql.PublicSpi; import graphql.execution.ExecutionContext; -import graphql.execution.instrumentation.adapters.ExecutionResultInstrumentationContextAdapter; -import graphql.execution.instrumentation.adapters.ExecuteObjectInstrumentationContextAdapter; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; @@ -42,34 +40,6 @@ */ @PublicSpi public interface Instrumentation { - - /** - * This will be called just before execution to create an object that is given back to all instrumentation methods - * to allow them to have per execution request state - * - * @return a state object that is passed to each method - * - * @deprecated use {@link #createState(InstrumentationCreateStateParameters)} instead - */ - @Deprecated(since = "2022-07-26") - default InstrumentationState createState() { - return null; - } - - /** - * This will be called just before execution to create an object that is given back to all instrumentation methods - * to allow them to have per execution request state - * - * @param parameters the parameters to this step - * - * @return a state object that is passed to each method - */ - @Deprecated(since = "2023-08-25") - @Nullable - default InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return createState(); - } - /** * This will be called just before execution to create an object, in an asynchronous manner, that is given back to all instrumentation methods * to allow them to have per execution request state @@ -80,49 +50,20 @@ default InstrumentationState createState(InstrumentationCreateStateParameters pa */ @Nullable default CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { - return CompletableFuture.completedFuture(createState(parameters)); + return null; } - /** - * This is called right at the start of query execution, and it's the first step in the instrumentation chain. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginExecution(InstrumentationExecutionParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { - return noOp(); - } /** * This is called right at the start of query execution, and it's the first step in the instrumentation chain. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) { - return beginExecution(parameters.withNewState(state)); - } - - /** - * This is called just before a query is parsed. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginParse(InstrumentationExecutionParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { return noOp(); } @@ -130,27 +71,12 @@ default InstrumentationContext beginParse(InstrumentationExecutionPara * This is called just before a query is parsed. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { - return beginParse(parameters.withNewState(state)); - } - - /** - * This is called just before the parsed query document is validated. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginValidation(InstrumentationValidationParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { return noOp(); } @@ -158,27 +84,12 @@ default InstrumentationContext> beginValidation(Instrument * This is called just before the parsed query document is validated. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { - return beginValidation(parameters.withNewState(state)); - } - - /** - * This is called just before the execution of the query operation is started. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginExecuteOperation(InstrumentationExecuteOperationParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { return noOp(); } @@ -186,29 +97,13 @@ default InstrumentationContext beginExecuteOperation(Instrument * This is called just before the execution of the query operation is started. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { - return beginExecuteOperation(parameters.withNewState(state)); - } - - /** - * This is called each time an {@link graphql.execution.ExecutionStrategy} is invoked, which may be multiple times - * per query as the engine recursively descends over the query. - * - * @param parameters the parameters to this step - * - * @return a non null {@link ExecutionStrategyInstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginExecutionStrategy(InstrumentationExecutionStrategyParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - return ExecutionStrategyInstrumentationContext.NOOP; + return noOp(); } /** @@ -216,13 +111,13 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen * per query as the engine recursively descends over the query. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link ExecutionStrategyInstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { - return beginExecutionStrategy(parameters.withNewState(state)); + return ExecutionStrategyInstrumentationContext.NOOP; } /** @@ -230,7 +125,7 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen * per query as the engine recursively descends over the query. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link ExecutionStrategyInstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @@ -244,7 +139,8 @@ default ExecuteObjectInstrumentationContext beginExecuteObject(InstrumentationEx *

* This is an EXPERIMENTAL instrumentation callback. The method signature will definitely change. * - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} + * * @return a nullable {@link ExecutionStrategyInstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @ExperimentalApi @@ -256,42 +152,12 @@ default InstrumentationContext beginDeferredField(InstrumentationState s * This is called each time a subscription field produces a new reactive stream event value and it needs to be mapped over via the graphql field subselection. * * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginSubscribedFieldEvent(InstrumentationFieldParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { - return noOp(); - } - - /** - * This is called each time a subscription field produces a new reactive stream event value and it needs to be mapped over via the graphql field subselection. - * - * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { - return beginSubscribedFieldEvent(parameters.withNewState(state)); - } - - /** - * This is called just before a field is resolved into a value. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginField(InstrumentationFieldParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginField(InstrumentationFieldParameters parameters) { return noOp(); } @@ -299,71 +165,26 @@ default InstrumentationContext beginField(InstrumentationFieldP * This is called just before a field is resolved into a value. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} - * - * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) - */ - @Deprecated(since="2023-09-11" ) - @Nullable - default InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { - return beginField(parameters.withNewState(state)); - } - - /** - * This is called just before a field is resolved into a value. - * - * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { - InstrumentationContext ic = beginField(parameters, state); - return ic == null ? null : new ExecutionResultInstrumentationContextAdapter(ic); - } - - /** - * This is called just before a field {@link DataFetcher} is invoked. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginFieldFetch(InstrumentationFieldFetchParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { return noOp(); } + /** * This is called just before a field {@link DataFetcher} is invoked. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { - return beginFieldFetch(parameters.withNewState(state)); - } - - - /** - * This is called just before the complete field is started. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginFieldComplete(InstrumentationFieldCompleteParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { return noOp(); } @@ -371,42 +192,12 @@ default InstrumentationContext beginFieldComplete(Instrumentati * This is called just before the complete field is started. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} - * - * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) - */ - @Deprecated(since = "2023-09-11") - @Nullable - default InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return beginFieldComplete(parameters.withNewState(state)); - } - - /** - * This is called just before the complete field is started. - * - * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - InstrumentationContext ic = beginFieldComplete(parameters, state); - return ic == null ? null : new ExecutionResultInstrumentationContextAdapter(ic); - } - - /** - * This is called just before the complete field list is started. - * - * @param parameters the parameters to this step - * - * @return a non null {@link InstrumentationContext} object that will be called back when the step ends - * - * @deprecated use {@link #beginFieldListComplete(InstrumentationFieldCompleteParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { return noOp(); } @@ -414,45 +205,13 @@ default InstrumentationContext beginFieldListComplete(Instrumen * This is called just before the complete field list is started. * * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} - * - * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) - */ - @Deprecated(since = "2023-09-11") - @Nullable - default InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return beginFieldListComplete(parameters.withNewState(state)); - } - - /** - * This is called just before the complete field list is started. - * - * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) */ @Nullable default InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - InstrumentationContext ic = beginFieldListComplete(parameters, state); - return ic == null ? null : new ExecutionResultInstrumentationContextAdapter(ic); - } - - /** - * This is called to instrument a {@link graphql.ExecutionInput} before it is used to parse, validate - * and execute a query, allowing you to adjust what query input parameters are used - * - * @param executionInput the execution input to be used - * @param parameters the parameters describing the field to be fetched - * - * @return a non null instrumented ExecutionInput, the default is to return to the same object - * - * @deprecated use {@link #instrumentExecutionInput(ExecutionInput, InstrumentationExecutionParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { - return executionInput; + return noOp(); } /** @@ -461,29 +220,13 @@ default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, I * * @param executionInput the execution input to be used * @param parameters the parameters describing the field to be fetched - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * - * @return a non null instrumented ExecutionInput, the default is to return to the same object + * @return a non-null instrumented ExecutionInput, the default is to return to the same object */ @NotNull default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { - return instrumentExecutionInput(executionInput, parameters.withNewState(state)); - } - - /** - * This is called to instrument a {@link graphql.language.Document} and variables before it is used allowing you to adjust the query AST if you so desire - * - * @param documentAndVariables the document and variables to be used - * @param parameters the parameters describing the execution - * - * @return a non null instrumented DocumentAndVariables, the default is to return to the same objects - * - * @deprecated use {@link #instrumentDocumentAndVariables(DocumentAndVariables, InstrumentationExecutionParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { - return documentAndVariables; + return executionInput; } /** @@ -491,30 +234,13 @@ default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables * * @param documentAndVariables the document and variables to be used * @param parameters the parameters describing the execution - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * - * @return a non null instrumented DocumentAndVariables, the default is to return to the same objects + * @return a non-null instrumented DocumentAndVariables, the default is to return to the same objects */ @NotNull default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters, InstrumentationState state) { - return instrumentDocumentAndVariables(documentAndVariables, parameters.withNewState(state)); - } - - /** - * This is called to instrument a {@link graphql.schema.GraphQLSchema} before it is used to parse, validate - * and execute a query, allowing you to adjust what types are used. - * - * @param schema the schema to be used - * @param parameters the parameters describing the field to be fetched - * - * @return a non null instrumented GraphQLSchema, the default is to return to the same object - * - * @deprecated use {@link #instrumentSchema(GraphQLSchema, InstrumentationExecutionParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { - return schema; + return documentAndVariables; } /** @@ -523,30 +249,13 @@ default GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExec * * @param schema the schema to be used * @param parameters the parameters describing the field to be fetched - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * - * @return a non null instrumented GraphQLSchema, the default is to return to the same object + * @return a non-null instrumented GraphQLSchema, the default is to return to the same object */ @NotNull default GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) { - return instrumentSchema(schema, parameters.withNewState(state)); - } - - /** - * This is called to instrument a {@link ExecutionContext} before it is used to execute a query, - * allowing you to adjust the base data used. - * - * @param executionContext the execution context to be used - * @param parameters the parameters describing the field to be fetched - * - * @return a non null instrumented ExecutionContext, the default is to return to the same object - * - * @deprecated use {@link #instrumentExecutionContext(ExecutionContext, InstrumentationExecutionParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { - return executionContext; + return schema; } /** @@ -555,32 +264,13 @@ default ExecutionContext instrumentExecutionContext(ExecutionContext executionCo * * @param executionContext the execution context to be used * @param parameters the parameters describing the field to be fetched - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * - * @return a non null instrumented ExecutionContext, the default is to return to the same object + * @return a non-null instrumented ExecutionContext, the default is to return to the same object */ @NotNull default ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { - return instrumentExecutionContext(executionContext, parameters.withNewState(state)); - } - - /** - * This is called to instrument a {@link DataFetcher} just before it is used to fetch a field, allowing you - * to adjust what information is passed back or record information about specific data fetches. Note - * the same data fetcher instance maybe presented to you many times and that data fetcher - * implementations widely vary. - * - * @param dataFetcher the data fetcher about to be used - * @param parameters the parameters describing the field to be fetched - * - * @return a non null instrumented DataFetcher, the default is to return to the same object - * - * @deprecated use {@link #instrumentDataFetcher(DataFetcher, InstrumentationFieldFetchParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { - return dataFetcher; + return executionContext; } /** @@ -591,29 +281,13 @@ default DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrum * * @param dataFetcher the data fetcher about to be used * @param parameters the parameters describing the field to be fetched - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * - * @return a non null instrumented DataFetcher, the default is to return to the same object + * @return a non-null instrumented DataFetcher, the default is to return to the same object */ @NotNull default DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { - return instrumentDataFetcher(dataFetcher, parameters.withNewState(state)); - } - - /** - * This is called to allow instrumentation to instrument the execution result in some way - * - * @param executionResult {@link java.util.concurrent.CompletableFuture} of the result to instrument - * @param parameters the parameters to this step - * - * @return a new execution result completable future - * - * @deprecated use {@link #instrumentExecutionResult(ExecutionResult, InstrumentationExecutionParameters, InstrumentationState)} instead - */ - @Deprecated(since = "2022-07-26") - @NotNull - default CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { - return CompletableFuture.completedFuture(executionResult); + return dataFetcher; } /** @@ -621,12 +295,12 @@ default CompletableFuture instrumentExecutionResult(ExecutionRe * * @param executionResult {@link java.util.concurrent.CompletableFuture} of the result to instrument * @param parameters the parameters to this step - * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a new execution result completable future */ @NotNull default CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { - return instrumentExecutionResult(executionResult, parameters.withNewState(state)); + return CompletableFuture.completedFuture(executionResult); } } diff --git a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java index 6e6ec887a8..61a15a1247 100644 --- a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java @@ -46,25 +46,9 @@ public class SimplePerformantInstrumentation implements Instrumentation { */ public static final SimplePerformantInstrumentation INSTANCE = new SimplePerformantInstrumentation(); - @Override - public InstrumentationState createState() { - return assertShouldNeverHappen("The deprecated " + "createState" + " was called"); - } - - @Override - public @Nullable InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return null; - } - @Override public @Nullable CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { - InstrumentationState state = createState(parameters); - return state == null ? null : CompletableFuture.completedFuture(state); - } - - @Override - public @NotNull InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginExecution" + " was called"); + return null; } @Override @@ -72,41 +56,21 @@ public InstrumentationState createState() { return noOp(); } - @Override - public @NotNull InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginParse" + " was called"); - } - @Override public @Nullable InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginValidation" + " was called"); - } - @Override public @Nullable InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginExecuteOperation" + " was called"); - } - @Override public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginExecutionStrategy" + " was called"); - } - @Override public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { return ExecutionStrategyInstrumentationContext.NOOP; @@ -117,126 +81,57 @@ public InstrumentationState createState() { return ExecuteObjectInstrumentationContext.NOOP; } - @Override - public @NotNull InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginSubscribedFieldEvent" + " was called"); - } - @Override public @Nullable InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull InstrumentationContext beginField(InstrumentationFieldParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginField" + " was called"); - } - - @Override - public @Nullable InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { - return noOp(); - } - @Override public @Nullable InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginFieldFetch" + " was called"); - } - @Override public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginFieldComplete" + " was called"); - } - - @Override - public @Nullable InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return noOp(); - } @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "beginFieldListComplete" + " was called"); - } - - @Override - public @Nullable InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return noOp(); - } - @Override public @Nullable InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return noOp(); } - @Override - public @NotNull ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "instrumentExecutionInput" + " was called"); - } - @Override public @NotNull ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { return executionInput; } - @Override - public @NotNull DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "instrumentDocumentAndVariables" + " was called"); - } - @Override public @NotNull DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters, InstrumentationState state) { return documentAndVariables; } - @Override - public @NotNull GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "instrumentSchema" + " was called"); - } - @Override public @NotNull GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) { return schema; } - @Override - public @NotNull ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "instrumentExecutionContext" + " was called"); - } - @Override public @NotNull ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { return executionContext; } - @Override - public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "instrumentDataFetcher" + " was called"); - } - @Override public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return dataFetcher; } - @Override - public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { - return assertShouldNeverHappen("The deprecated " + "instrumentExecutionResult" + " was called"); - } - @Override public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { return CompletableFuture.completedFuture(executionResult); diff --git a/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java b/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java deleted file mode 100644 index 2c42d55906..0000000000 --- a/src/main/java/graphql/execution/instrumentation/adapters/ExecuteObjectInstrumentationContextAdapter.java +++ /dev/null @@ -1,49 +0,0 @@ -package graphql.execution.instrumentation.adapters; - -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.Internal; -import graphql.execution.FieldValueInfo; -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -/** - * A class to help adapt old {@link ExecutionResult} based ExecutionStrategyInstrumentationContext - * from the newer {@link Map} based ones. - */ -@Internal -public class ExecuteObjectInstrumentationContextAdapter implements ExecuteObjectInstrumentationContext { - - private final ExecutionStrategyInstrumentationContext delegate; - - public ExecuteObjectInstrumentationContextAdapter(ExecutionStrategyInstrumentationContext delegate) { - this.delegate = delegate; - } - - @Override - public void onDispatched(CompletableFuture> result) { - CompletableFuture future = result.thenApply(r -> ExecutionResultImpl.newExecutionResult().data(r).build()); - delegate.onDispatched(future); - // - // when the mapped future is completed, then call onCompleted on the delegate - future.whenComplete(delegate::onCompleted); - } - - @Override - public void onCompleted(Map result, Throwable t) { - } - - @Override - public void onFieldValuesInfo(List fieldValueInfoList) { - delegate.onFieldValuesInfo(fieldValueInfoList); - } - - @Override - public void onFieldValuesException() { - delegate.onFieldValuesException(); - } -} diff --git a/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java b/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java deleted file mode 100644 index d67eddb8d0..0000000000 --- a/src/main/java/graphql/execution/instrumentation/adapters/ExecutionResultInstrumentationContextAdapter.java +++ /dev/null @@ -1,35 +0,0 @@ -package graphql.execution.instrumentation.adapters; - -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.Internal; -import graphql.execution.instrumentation.InstrumentationContext; - -import java.util.concurrent.CompletableFuture; - -/** - * A class to help adapt old {@link ExecutionResult} based InstrumentationContext - * from the newer {@link Object} based ones. - */ -@Internal -public class ExecutionResultInstrumentationContextAdapter implements InstrumentationContext { - - private final InstrumentationContext delegate; - - public ExecutionResultInstrumentationContextAdapter(InstrumentationContext delegate) { - this.delegate = delegate; - } - - @Override - public void onDispatched(CompletableFuture result) { - CompletableFuture future = result.thenApply(obj -> ExecutionResultImpl.newExecutionResult().data(obj).build()); - delegate.onDispatched(future); - // - // when the mapped future is completed, then call onCompleted on the delegate - future.whenComplete(delegate::onCompleted); - } - - @Override - public void onCompleted(Object result, Throwable t) { - } -} diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 6f179d5617..da4cdf37ec 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -68,8 +68,8 @@ public DataLoaderDispatcherInstrumentation(DataLoaderDispatcherInstrumentationOp @Override - public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return new DataLoaderDispatcherInstrumentationState(parameters.getExecutionInput().getDataLoaderRegistry()); + public @Nullable CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + return CompletableFuture.completedFuture(new DataLoaderDispatcherInstrumentationState(parameters.getExecutionInput().getDataLoaderRegistry())); } @Override diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java index 4de6a1164c..c2b3029732 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java @@ -12,48 +12,13 @@ @PublicApi public class InstrumentationExecuteOperationParameters { private final ExecutionContext executionContext; - private final InstrumentationState instrumentationState; - public InstrumentationExecuteOperationParameters(ExecutionContext executionContext) { - this(executionContext, executionContext.getInstrumentationState()); - } - - private InstrumentationExecuteOperationParameters(ExecutionContext executionContext, InstrumentationState instrumentationState) { this.executionContext = executionContext; - this.instrumentationState = instrumentationState; } - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - public InstrumentationExecuteOperationParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationExecuteOperationParameters(executionContext, instrumentationState); - } public ExecutionContext getExecutionContext() { return executionContext; } - /** - * Previously the instrumentation parameters had access to the state created via {@link Instrumentation#createState(InstrumentationCreateStateParameters)} but now - * to save object allocations, the state is passed directly into instrumentation methods - * - * @param for two - * - * @return the state created previously during a call to {@link Instrumentation#createState(InstrumentationCreateStateParameters)} - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - public T getInstrumentationState() { - //noinspection unchecked - return (T) instrumentationState; - } } diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java index 10cce06031..caa48ac7c5 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java @@ -21,33 +21,18 @@ public class InstrumentationExecutionParameters { private final Object context; private final GraphQLContext graphQLContext; private final Map variables; - private final InstrumentationState instrumentationState; private final GraphQLSchema schema; - public InstrumentationExecutionParameters(ExecutionInput executionInput, GraphQLSchema schema, InstrumentationState instrumentationState) { + public InstrumentationExecutionParameters(ExecutionInput executionInput, GraphQLSchema schema) { this.executionInput = executionInput; this.query = executionInput.getQuery(); this.operation = executionInput.getOperationName(); this.context = executionInput.getContext(); this.graphQLContext = executionInput.getGraphQLContext(); this.variables = executionInput.getVariables() != null ? executionInput.getVariables() : ImmutableKit.emptyMap(); - this.instrumentationState = instrumentationState; this.schema = schema; } - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - public InstrumentationExecutionParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationExecutionParameters(this.getExecutionInput(), this.schema, instrumentationState); - } public ExecutionInput getExecutionInput() { return executionInput; @@ -82,22 +67,6 @@ public Map getVariables() { return variables; } - /** - * Previously the instrumentation parameters had access to the state created via {@link Instrumentation#createState(InstrumentationCreateStateParameters)} but now - * to save object allocations, the state is passed directly into instrumentation methods - * - * @param for two - * - * @return the state created previously during a call to {@link Instrumentation#createState(InstrumentationCreateStateParameters)} - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - @SuppressWarnings("TypeParameterUnusedInFormals") - public T getInstrumentationState() { - //noinspection unchecked - return (T) instrumentationState; - } public GraphQLSchema getSchema() { return this.schema; diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java index a2ce253cdc..9c93c84d42 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java @@ -3,8 +3,6 @@ import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; -import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.instrumentation.InstrumentationState; /** * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods @@ -14,31 +12,12 @@ public class InstrumentationExecutionStrategyParameters { private final ExecutionContext executionContext; private final ExecutionStrategyParameters executionStrategyParameters; - private final InstrumentationState instrumentationState; public InstrumentationExecutionStrategyParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters) { - this(executionContext, executionStrategyParameters, executionContext.getInstrumentationState()); - } - - private InstrumentationExecutionStrategyParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, InstrumentationState instrumentationState) { this.executionContext = executionContext; this.executionStrategyParameters = executionStrategyParameters; - this.instrumentationState = instrumentationState; } - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - public InstrumentationExecutionStrategyParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationExecutionStrategyParameters(executionContext, executionStrategyParameters, instrumentationState); - } public ExecutionContext getExecutionContext() { return executionContext; @@ -48,20 +27,4 @@ public ExecutionStrategyParameters getExecutionStrategyParameters() { return executionStrategyParameters; } - /** - * Previously the instrumentation parameters had access to the state created via {@link Instrumentation#createState(InstrumentationCreateStateParameters)} but now - * to save object allocations, the state is passed directly into instrumentation methods - * - * @param for two - * - * @return the state created previously during a call to {@link Instrumentation#createState(InstrumentationCreateStateParameters)} - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - @SuppressWarnings("TypeParameterUnusedInFormals") - public T getInstrumentationState() { - //noinspection unchecked - return (T) instrumentationState; - } } diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java index dd5d2244d4..1687c3d149 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java @@ -18,35 +18,15 @@ public class InstrumentationFieldCompleteParameters { private final ExecutionContext executionContext; private final Supplier executionStepInfo; private final Object fetchedValue; - private final InstrumentationState instrumentationState; private final ExecutionStrategyParameters executionStrategyParameters; public InstrumentationFieldCompleteParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, Supplier executionStepInfo, Object fetchedValue) { - this(executionContext, executionStrategyParameters, executionStepInfo, fetchedValue, executionContext.getInstrumentationState()); - } - - InstrumentationFieldCompleteParameters(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, Supplier executionStepInfo, Object fetchedValue, InstrumentationState instrumentationState) { this.executionContext = executionContext; this.executionStrategyParameters = executionStrategyParameters; this.executionStepInfo = executionStepInfo; this.fetchedValue = fetchedValue; - this.instrumentationState = instrumentationState; } - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - public InstrumentationFieldCompleteParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationFieldCompleteParameters( - this.executionContext, executionStrategyParameters, this.executionStepInfo, this.fetchedValue, instrumentationState); - } public ExecutionContext getExecutionContext() { @@ -74,20 +54,4 @@ public Object getFetchedValue() { return fetchedValue; } - /** - * Previously the instrumentation parameters had access to the state created via {@link Instrumentation#createState(InstrumentationCreateStateParameters)} but now - * to save object allocations, the state is passed directly into instrumentation methods - * - * @param for two - * - * @return the state created previously during a call to {@link Instrumentation#createState(InstrumentationCreateStateParameters)} - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - @SuppressWarnings("TypeParameterUnusedInFormals") - public T getInstrumentationState() { - //noinspection unchecked - return (T) instrumentationState; - } } diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java index 5c54cd3e98..d8eedb100c 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java @@ -25,31 +25,6 @@ public InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext, this.trivialDataFetcher = trivialDataFetcher; } - private InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext, Supplier environment, InstrumentationState instrumentationState, ExecutionStrategyParameters executionStrategyParameters, boolean trivialDataFetcher) { - super(getExecutionContext, () -> environment.get().getExecutionStepInfo(), instrumentationState); - this.environment = environment; - this.executionStrategyParameters = executionStrategyParameters; - this.trivialDataFetcher = trivialDataFetcher; - } - - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - @Override - public InstrumentationFieldFetchParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationFieldFetchParameters( - this.getExecutionContext(), this.environment, - instrumentationState, executionStrategyParameters, trivialDataFetcher); - } - - public DataFetchingEnvironment getEnvironment() { return environment.get(); } diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java index 9c6342d2c7..6bbfcbe9e1 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java @@ -16,34 +16,10 @@ public class InstrumentationFieldParameters { private final ExecutionContext executionContext; private final Supplier executionStepInfo; - private final InstrumentationState instrumentationState; - public InstrumentationFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo) { - this(executionContext, executionStepInfo, executionContext.getInstrumentationState()); - } - - InstrumentationFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, InstrumentationState instrumentationState) { this.executionContext = executionContext; this.executionStepInfo = executionStepInfo; - this.instrumentationState = instrumentationState; - } - - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - public InstrumentationFieldParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationFieldParameters( - this.executionContext, this.executionStepInfo, instrumentationState); } - - public ExecutionContext getExecutionContext() { return executionContext; } @@ -56,20 +32,4 @@ public ExecutionStepInfo getExecutionStepInfo() { return executionStepInfo.get(); } - /** - * Previously the instrumentation parameters had access to the state created via {@link Instrumentation#createState(InstrumentationCreateStateParameters)} but now - * to save object allocations, the state is passed directly into instrumentation methods - * - * @param for two - * - * @return the state created previously during a call to {@link Instrumentation#createState(InstrumentationCreateStateParameters)} - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - @SuppressWarnings("TypeParameterUnusedInFormals") - public T getInstrumentationState() { - //noinspection unchecked - return (T) instrumentationState; - } } diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java index 713b75c63c..0905875f4e 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java @@ -14,27 +14,11 @@ public class InstrumentationValidationParameters extends InstrumentationExecutionParameters { private final Document document; - public InstrumentationValidationParameters(ExecutionInput executionInput, Document document, GraphQLSchema schema, InstrumentationState instrumentationState) { - super(executionInput, schema, instrumentationState); + public InstrumentationValidationParameters(ExecutionInput executionInput, Document document, GraphQLSchema schema) { + super(executionInput, schema); this.document = document; } - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - * - * @deprecated state is now passed in direct to instrumentation methods - */ - @Deprecated(since = "2022-07-26") - @Override - public InstrumentationValidationParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationValidationParameters( - this.getExecutionInput(), document, getSchema(), instrumentationState); - } - public Document getDocument() { return document; diff --git a/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java b/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java index 9750ec1475..daa67d4ea7 100644 --- a/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java @@ -72,8 +72,8 @@ public TracingInstrumentation(Options options) { private final Options options; @Override - public @Nullable InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return new TracingSupport(options.includeTrivialDataFetchers); + public @Nullable CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + return CompletableFuture.completedFuture(new TracingSupport(options.includeTrivialDataFetchers)); } @Override diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index d0d1f1b636..ddca15c727 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -1070,7 +1070,9 @@ many lines'''] InstrumentationCreateStateParameters seenParams def instrumentation = new Instrumentation() { - @Override InstrumentationState createState(InstrumentationCreateStateParameters params) { + + @Override + CompletableFuture createStateAsync(InstrumentationCreateStateParameters params) { seenParams = params null } diff --git a/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy b/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy index cee882ccae..798b9e5512 100644 --- a/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy +++ b/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy @@ -167,7 +167,7 @@ class MaxQueryComplexityInstrumentationTest extends Specification { private InstrumentationExecuteOperationParameters createExecuteOperationParameters(MaxQueryComplexityInstrumentation queryComplexityInstrumentation, ExecutionInput executionInput, Document query, GraphQLSchema schema, InstrumentationState state) { // we need to run N steps to create instrumentation state - def validationParameters = new InstrumentationValidationParameters(executionInput, query, schema, state) + def validationParameters = new InstrumentationValidationParameters(executionInput, query, schema) queryComplexityInstrumentation.beginValidation(validationParameters, state) def executionContext = executionCtx(executionInput, query, schema) def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext) @@ -175,7 +175,7 @@ class MaxQueryComplexityInstrumentationTest extends Specification { } def createInstrumentationState(MaxQueryComplexityInstrumentation queryComplexityInstrumentation) { - queryComplexityInstrumentation.createState(null) + queryComplexityInstrumentation.createStateAsync(null).join() } diff --git a/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy b/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy index 942bee10dc..acc1a08983 100644 --- a/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy +++ b/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy @@ -67,7 +67,7 @@ class MaxQueryDepthInstrumentationTest extends Specification { ExecutionInput executionInput = Mock(ExecutionInput) def executionContext = executionCtx(executionInput, query, schema) def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext) - def state = maximumQueryDepthInstrumentation.createState(null) + def state = null // it has not state in implementation when: maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters, state) then: diff --git a/src/test/groovy/graphql/execution/SubscriptionExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/SubscriptionExecutionStrategyTest.groovy index 4a8031a805..c6e0f5a52b 100644 --- a/src/test/groovy/graphql/execution/SubscriptionExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/SubscriptionExecutionStrategyTest.groovy @@ -8,6 +8,7 @@ import graphql.GraphQLError import graphql.GraphqlErrorBuilder import graphql.TestUtil import graphql.TypeMismatchError +import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.LegacyTestingInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.execution.pubsub.CapturingSubscriber @@ -579,7 +580,7 @@ class SubscriptionExecutionStrategyTest extends Specification { def instrumentResultCalls = [] LegacyTestingInstrumentation instrumentation = new LegacyTestingInstrumentation() { @Override - CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { + CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { instrumentResultCalls.add("instrumentExecutionResult") return CompletableFuture.completedFuture(executionResult) } diff --git a/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy index c430c10851..d702335c4d 100644 --- a/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/AllNullTestingInstrumentation.groovy @@ -25,11 +25,11 @@ class AllNullTestingInstrumentation implements Instrumentation { List executionList = [] List dfInvocations = [] List dfClasses = [] - Map capturedData = [:] + Map capturedData = [:] @Override - InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return instrumentationState + CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + return CompletableFuture.completedFuture(instrumentationState) } @Override @@ -82,7 +82,7 @@ class AllNullTestingInstrumentation implements Instrumentation { } @Override - InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { + InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { assert state == instrumentationState executionList << "start:field-$parameters.field.name" return null @@ -96,14 +96,14 @@ class AllNullTestingInstrumentation implements Instrumentation { } @Override - InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { assert state == instrumentationState executionList << "start:complete-$parameters.field.name" return null } @Override - InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { assert state == instrumentationState executionList << "start:complete-list-$parameters.field.name" return null diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index 9da4e4990c..5e931e000c 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -127,8 +127,9 @@ class InstrumentationTest extends Specification { """ def instrumentation = new LegacyTestingInstrumentation() { + @Override - DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { + DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return new DataFetcher() { @Override Object get(DataFetchingEnvironment environment) { diff --git a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy index 640aa32c56..eb43263e72 100644 --- a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy @@ -3,6 +3,7 @@ package graphql.execution.instrumentation import graphql.ExecutionInput import graphql.ExecutionResult import graphql.execution.ExecutionContext +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters @@ -33,31 +34,31 @@ class LegacyTestingInstrumentation implements Instrumentation { def useOnDispatch = false @Override - InstrumentationState createState() { - return instrumentationState + CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + return CompletableFuture.completedFuture(instrumentationState) } @Override - InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) { + assert state == instrumentationState new TestingInstrumentContext("execution", executionList, throwableList, useOnDispatch) } @Override - InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("parse", executionList, throwableList, useOnDispatch) } @Override - InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("validation", executionList, throwableList, useOnDispatch) } @Override - ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingExecutionStrategyInstrumentationContext("execution-strategy", executionList, throwableList, useOnDispatch) } @@ -67,62 +68,62 @@ class LegacyTestingInstrumentation implements Instrumentation { } @Override - InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("execute-operation", executionList, throwableList, useOnDispatch) } @Override - InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("subscribed-field-event-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override - InstrumentationContext beginField(InstrumentationFieldParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("field-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override - InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("fetch-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override - InstrumentationContext> beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("complete-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override - InstrumentationContext> beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + assert state == instrumentationState return new TestingInstrumentContext("complete-list-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override - GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) { + assert state == instrumentationState return schema } @Override - ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { + assert state == instrumentationState return executionInput } @Override - ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { + assert state == instrumentationState return executionContext } @Override - DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + assert state == instrumentationState dfClasses.add(dataFetcher.getClass()) return new DataFetcher() { @Override @@ -134,8 +135,8 @@ class LegacyTestingInstrumentation implements Instrumentation { } @Override - CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage + CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { + assert state == instrumentationState return CompletableFuture.completedFuture(executionResult) } } diff --git a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy index 0053f595dc..5d6fbb1d8e 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy @@ -33,8 +33,8 @@ class ModernTestingInstrumentation implements Instrumentation { boolean useOnDispatch = false @Override - InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return instrumentationState + CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + return CompletableFuture.completedFuture(instrumentationState) } @Override @@ -79,12 +79,6 @@ class ModernTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("subscribed-field-event-$parameters.field.name", executionList, throwableList, useOnDispatch) } - @Override - InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { - assert state == instrumentationState - return new TestingInstrumentContext("field-$parameters.field.name", executionList, throwableList, useOnDispatch) - } - @Override InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { assert state == instrumentationState @@ -97,24 +91,12 @@ class ModernTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("fetch-$parameters.field.name", executionList, throwableList, useOnDispatch) } - @Override - InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - assert state == instrumentationState - return new TestingInstrumentContext("complete-$parameters.field.name", executionList, throwableList, useOnDispatch) - } - @Override InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { assert state == instrumentationState return new TestingInstrumentContext("complete-$parameters.field.name", executionList, throwableList, useOnDispatch) } - @Override - InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - assert state == instrumentationState - return new TestingInstrumentContext("complete-list-$parameters.field.name", executionList, throwableList, useOnDispatch) - } - @Override InstrumentationContext beginFieldListCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { assert state == instrumentationState diff --git a/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy index 42157eaa86..5e17e040ad 100644 --- a/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy @@ -1,6 +1,7 @@ package graphql.execution.instrumentation import graphql.ExecutionResult +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters @@ -26,8 +27,8 @@ class NamedInstrumentation extends ModernTestingInstrumentation { } @Override - InstrumentationState createState() { - return instrumentationState + CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + return CompletableFuture.completedFuture(instrumentationState) } def assertState(InstrumentationState instrumentationState) { @@ -65,12 +66,6 @@ class NamedInstrumentation extends ModernTestingInstrumentation { return super.beginExecuteOperation(parameters, state) } - @Override - InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { - assertState(state) - return super.beginField(parameters, state) - } - @Override InstrumentationContext beginFieldExecution(InstrumentationFieldParameters parameters, InstrumentationState state) { assertState(state) diff --git a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy index 06034eb2c3..aefcaf3293 100644 --- a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy @@ -311,7 +311,7 @@ class FieldValidationTest extends Specification { def execution = new Execution(strategy, strategy, strategy, instrumentation, ValueUnboxer.DEFAULT) def executionInput = ExecutionInput.newExecutionInput().query(query).variables(variables).build() - execution.execute(document, schema, ExecutionId.generate(), executionInput, SimplePerformantInstrumentation.INSTANCE.createState(new InstrumentationCreateStateParameters(schema, executionInput))) + execution.execute(document, schema, ExecutionId.generate(), executionInput, null) } def "test graphql from end to end with chained instrumentation"() { diff --git a/src/test/groovy/readme/InstrumentationExamples.java b/src/test/groovy/readme/InstrumentationExamples.java index f404ca4ee5..60ad4dc4dc 100644 --- a/src/test/groovy/readme/InstrumentationExamples.java +++ b/src/test/groovy/readme/InstrumentationExamples.java @@ -64,13 +64,14 @@ void recordTiming(String key, long time) { } class CustomInstrumentation extends SimplePerformantInstrumentation { + @Override - public @Nullable InstrumentationState createState(InstrumentationCreateStateParameters parameters) { + public @Nullable CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { // // instrumentation state is passed during each invocation of an Instrumentation method // and allows you to put stateful data away and reference it during the query execution // - return new CustomInstrumentationState(); + return CompletableFuture.completedFuture(new CustomInstrumentationState()); } @Override From b26b19de87c04f989c7119ca28c57ce9d57e06f8 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Mon, 19 Feb 2024 10:56:14 -0800 Subject: [PATCH 225/393] Avoid repeated Map lookups in SimpleFieldValidation The validateFields method in SimpleFieldValidation would iterate over the keySet of the rules Map and then subsequently use the key to look up the value; switch to iterating over the entrySet. --- .../fieldvalidation/SimpleFieldValidation.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/fieldvalidation/SimpleFieldValidation.java b/src/main/java/graphql/execution/instrumentation/fieldvalidation/SimpleFieldValidation.java index 89634d064d..9f0a340f19 100644 --- a/src/main/java/graphql/execution/instrumentation/fieldvalidation/SimpleFieldValidation.java +++ b/src/main/java/graphql/execution/instrumentation/fieldvalidation/SimpleFieldValidation.java @@ -40,11 +40,12 @@ public SimpleFieldValidation addRule(ResultPath fieldPath, BiFunction validateFields(FieldValidationEnvironment validationEnvironment) { List errors = new ArrayList<>(); - for (ResultPath fieldPath : rules.keySet()) { + for (Map.Entry>> entry : rules.entrySet()) { + ResultPath fieldPath = entry.getKey(); + BiFunction> ruleFunction = entry.getValue(); + List fieldAndArguments = validationEnvironment.getFieldsByPath().get(fieldPath); if (fieldAndArguments != null) { - BiFunction> ruleFunction = rules.get(fieldPath); - for (FieldAndArguments fieldAndArgument : fieldAndArguments) { Optional graphQLError = ruleFunction.apply(fieldAndArgument, validationEnvironment); graphQLError.ifPresent(errors::add); From dd68781fe52ffe05ce96beb506ff1a142236f16e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 20 Feb 2024 15:00:55 +1100 Subject: [PATCH 226/393] Async can now wait on materialized objects --- src/main/java/graphql/execution/Async.java | 157 ++++++++++++++-- .../groovy/graphql/execution/AsyncTest.groovy | 171 +++++++++++++++++- 2 files changed, 307 insertions(+), 21 deletions(-) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index 56f7a2f9be..576f2f5cf0 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -23,13 +24,38 @@ public class Async { public interface CombinedBuilder { + /** + * This adds a {@link CompletableFuture} into the collection of results + * + * @param completableFuture the CF to add + */ void add(CompletableFuture completableFuture); + /** + * This can be either a materialized value or a {@link CompletableFuture} + * + * @param objectT the object to add + */ + void addObject(T objectT); + + /** + * This will return a {@code CompletableFuture>} even if the inputs are all materialized values + * + * @return a CompletableFuture to a List of values + */ CompletableFuture> await(); + + /** + * This will return a {@code CompletableFuture>} if ANY of the input values are async + * otherwise it just return a materialised {@code List} + * + * @return either a CompletableFuture or a materialized list + */ + /* CompletableFuture> | List */ Object awaitPolymorphic(); } /** - * Combines 0 or more CF into one. It is a wrapper around CompletableFuture.allOf. + * Combines zero or more CFs into one. It is a wrapper around CompletableFuture.allOf. * * @param expectedSize how many we expect * @param for two @@ -55,6 +81,10 @@ public void add(CompletableFuture completableFuture) { this.ix++; } + @Override + public void addObject(T objectT) { + this.ix++; + } @Override public CompletableFuture> await() { @@ -62,6 +92,11 @@ public CompletableFuture> await() { return typedEmpty(); } + @Override + public Object awaitPolymorphic() { + Assert.assertTrue(ix == 0, () -> "expected size was " + 0 + " got " + ix); + return Collections.emptyList(); + } // implementation details: infer the type of Completable> from a singleton empty private static final CompletableFuture> EMPTY = CompletableFuture.completedFuture(Collections.emptyList()); @@ -75,58 +110,140 @@ private static CompletableFuture typedEmpty() { private static class Single implements CombinedBuilder { // avoiding array allocation as there is only 1 CF - private CompletableFuture completableFuture; + private Object value; private int ix; @Override public void add(CompletableFuture completableFuture) { - this.completableFuture = completableFuture; + this.value = completableFuture; + this.ix++; + } + + @Override + public void addObject(T objectT) { + this.value = objectT; this.ix++; } @Override public CompletableFuture> await() { + commonSizeAssert(); + if (value instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletableFuture cf = (CompletableFuture) value; + return cf.thenApply(Collections::singletonList); + } + //noinspection unchecked + return CompletableFuture.completedFuture(Collections.singletonList((T) value)); + } + + @Override + public Object awaitPolymorphic() { + commonSizeAssert(); + if (value instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletableFuture cf = (CompletableFuture) value; + return cf.thenApply(Collections::singletonList); + } + //noinspection unchecked + return Collections.singletonList((T) value); + } + + private void commonSizeAssert() { Assert.assertTrue(ix == 1, () -> "expected size was " + 1 + " got " + ix); - return completableFuture.thenApply(Collections::singletonList); } } private static class Many implements CombinedBuilder { - private final CompletableFuture[] array; + private final Object[] array; private int ix; + private boolean containsCFs; @SuppressWarnings("unchecked") private Many(int size) { - this.array = new CompletableFuture[size]; + this.array = new Object[size]; this.ix = 0; + containsCFs = false; } @Override public void add(CompletableFuture completableFuture) { array[ix++] = completableFuture; + containsCFs = true; } + @Override + public void addObject(T objectT) { + array[ix++] = objectT; + if (objectT instanceof CompletableFuture) { + containsCFs = true; + } + } + + @SuppressWarnings("unchecked") @Override public CompletableFuture> await() { - Assert.assertTrue(ix == array.length, () -> "expected size was " + array.length + " got " + ix); + commonSizeAssert(); CompletableFuture> overallResult = new CompletableFuture<>(); - CompletableFuture.allOf(array) - .whenComplete((ignored, exception) -> { - if (exception != null) { - overallResult.completeExceptionally(exception); - return; - } - List results = new ArrayList<>(array.length); - for (CompletableFuture future : array) { - results.add(future.join()); - } - overallResult.complete(results); - }); + if (!containsCFs) { + overallResult.complete(materialisedList(array)); + } else { + CompletableFuture[] cfsArr = copyOnlyCFsToArray(); + CompletableFuture.allOf(cfsArr) + .whenComplete((ignored, exception) -> { + if (exception != null) { + overallResult.completeExceptionally(exception); + return; + } + List results = new ArrayList<>(array.length); + for (Object object : array) { + if (object instanceof CompletableFuture) { + CompletableFuture cf = (CompletableFuture) object; + // join is safe since they are all completed earlier via CompletableFuture.allOf() + results.add(cf.join()); + } else { + results.add((T) object); + } + } + overallResult.complete(results); + }); + } return overallResult; } + @Override + public Object awaitPolymorphic() { + if (!containsCFs) { + commonSizeAssert(); + return materialisedList(array); + } else { + return await(); + } + } + + @NotNull + private List materialisedList(Object[] array) { + List results = new ArrayList<>(array.length); + for (Object object : array) { + //noinspection unchecked + results.add((T) object); + } + return results; + } + + private void commonSizeAssert() { + Assert.assertTrue(ix == array.length, () -> "expected size was " + array.length + " got " + ix); + } + + @NotNull + private CompletableFuture[] copyOnlyCFsToArray() { + return Arrays.stream(array) + .filter(obj -> obj instanceof CompletableFuture) + .toArray(CompletableFuture[]::new); + } + } public static CompletableFuture> each(Collection list, Function> cfFactory) { @@ -220,4 +337,4 @@ public static CompletableFuture exceptionallyCompletedFuture(Throwable ex public static @NotNull CompletableFuture orNullCompletedFuture(@Nullable CompletableFuture completableFuture) { return completableFuture != null ? completableFuture : CompletableFuture.completedFuture(null); } -} +} \ No newline at end of file diff --git a/src/test/groovy/graphql/execution/AsyncTest.groovy b/src/test/groovy/graphql/execution/AsyncTest.groovy index e124f00220..0b58d40684 100644 --- a/src/test/groovy/graphql/execution/AsyncTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncTest.groovy @@ -4,8 +4,8 @@ import spock.lang.Specification import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException -import java.util.function.Function import java.util.function.BiFunction +import java.util.function.Function import static java.util.concurrent.CompletableFuture.completedFuture @@ -130,4 +130,173 @@ class AsyncTest extends Specification { exception.getCause().getMessage() == "some error" } + + def "can wait on objects of cfs or both"() { + when: + def asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.add(completedFuture("0")) + asyncBuilder.add(completedFuture("1")) + asyncBuilder.addObject("2") + asyncBuilder.addObject("3") + asyncBuilder.add(completedFuture("4")) + + def list = asyncBuilder.await().join() + + then: + list == ["0", "1", "2", "3", "4"] + + when: + asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.add(completedFuture("0")) + asyncBuilder.add(completedFuture("1")) + asyncBuilder.add(completedFuture("2")) + asyncBuilder.add(completedFuture("3")) + asyncBuilder.add(completedFuture("4")) + + list = asyncBuilder.await().join() + + then: + list == ["0", "1", "2", "3", "4"] + + when: + asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.addObject("0") + asyncBuilder.addObject("1") + asyncBuilder.addObject("2") + asyncBuilder.addObject("3") + asyncBuilder.addObject("4") + + list = asyncBuilder.await().join() + + then: + list == ["0", "1", "2", "3", "4"] + + when: "it has a mix of CFs and objects" + asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.addObject("0") + asyncBuilder.addObject("1") + asyncBuilder.add(completedFuture("2")) + asyncBuilder.addObject("3") + asyncBuilder.addObject(completedFuture("4")) + + list = asyncBuilder.await().join() + + then: + list == ["0", "1", "2", "3", "4"] + } + + def "can wait on objects of cfs or both with empty or single values"() { + when: + def asyncBuilder = Async.ofExpectedSize(0) + def list = asyncBuilder.await().join() + + then: + list == [] + + when: + asyncBuilder = Async.ofExpectedSize(1) + asyncBuilder.add(completedFuture("A")) + list = asyncBuilder.await().join() + + then: + list == ["A"] + + when: + asyncBuilder = Async.ofExpectedSize(1) + asyncBuilder.addObject(completedFuture("A")) + list = asyncBuilder.await().join() + + then: + list == ["A"] + + when: + asyncBuilder = Async.ofExpectedSize(1) + asyncBuilder.addObject("A") + list = asyncBuilder.await().join() + + then: + list == ["A"] + } + + def "await polymorphic works as expected"() { + + when: + def asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.add(completedFuture("0")) + asyncBuilder.add(completedFuture("1")) + asyncBuilder.addObject("2") + asyncBuilder.addObject("3") + asyncBuilder.add(completedFuture("4")) + + def awaited = asyncBuilder.awaitPolymorphic() + + then: + awaited instanceof CompletableFuture + joinOrMaterialized(awaited) == ["0", "1", "2", "3", "4"] + + when: + asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.addObject(completedFuture("0")) + asyncBuilder.addObject(completedFuture("1")) + asyncBuilder.addObject(completedFuture("2")) + asyncBuilder.addObject(completedFuture("3")) + asyncBuilder.addObject(completedFuture("4")) + + awaited = asyncBuilder.awaitPolymorphic() + + then: + awaited instanceof CompletableFuture + joinOrMaterialized(awaited) == ["0", "1", "2", "3", "4"] + + when: + asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.addObject("0") + asyncBuilder.addObject("1") + asyncBuilder.addObject("2") + asyncBuilder.addObject("3") + asyncBuilder.addObject("4") + + awaited = asyncBuilder.awaitPolymorphic() + + then: + !(awaited instanceof CompletableFuture) + joinOrMaterialized(awaited) == ["0", "1", "2", "3", "4"] + + when: + asyncBuilder = Async.ofExpectedSize(0) + + awaited = asyncBuilder.awaitPolymorphic() + + then: + !(awaited instanceof CompletableFuture) + joinOrMaterialized(awaited) == [] + + when: + asyncBuilder = Async.ofExpectedSize(1) + asyncBuilder.addObject("A") + + awaited = asyncBuilder.awaitPolymorphic() + + then: + !(awaited instanceof CompletableFuture) + joinOrMaterialized(awaited) == ["A"] + + when: + asyncBuilder = Async.ofExpectedSize(1) + asyncBuilder.addObject(completedFuture("A")) + + awaited = asyncBuilder.awaitPolymorphic() + + then: + awaited instanceof CompletableFuture + joinOrMaterialized(awaited) == ["A"] + } + + Object joinOrMaterialized(Object awaited) { + if (awaited instanceof CompletableFuture) { + return ((CompletableFuture) awaited).join() + } else { + return awaited + } + } } From d47b189e089a0b6b42b607bd808eeb39ff56d18e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 20 Feb 2024 16:23:16 +1100 Subject: [PATCH 227/393] javadoc problem --- .../execution/instrumentation/InstrumentationState.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/instrumentation/InstrumentationState.java b/src/main/java/graphql/execution/instrumentation/InstrumentationState.java index afea0f4afb..258c865474 100644 --- a/src/main/java/graphql/execution/instrumentation/InstrumentationState.java +++ b/src/main/java/graphql/execution/instrumentation/InstrumentationState.java @@ -1,12 +1,13 @@ package graphql.execution.instrumentation; import graphql.PublicSpi; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; /** * An {@link Instrumentation} implementation can create this as a stateful object that is then passed * to each instrumentation method, allowing state to be passed down with the request execution * - * @see Instrumentation#createState(graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters) + * @see Instrumentation#createStateAsync(InstrumentationCreateStateParameters) */ @PublicSpi public interface InstrumentationState { From c042821f7771bf5927bb8a69b183e4547f445b83 Mon Sep 17 00:00:00 2001 From: Danny Thomas Date: Tue, 20 Feb 2024 17:17:59 +1100 Subject: [PATCH 228/393] Add benchmark --- .../ChainedInstrumentationBenchmark.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/test/java/benchmark/ChainedInstrumentationBenchmark.java diff --git a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java new file mode 100644 index 0000000000..129d266765 --- /dev/null +++ b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java @@ -0,0 +1,77 @@ +package benchmark; + +import graphql.ExecutionInput; +import graphql.execution.instrumentation.ChainedInstrumentation; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import org.jetbrains.annotations.NotNull; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static graphql.Scalars.GraphQLString; +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; +import static graphql.schema.GraphQLObjectType.newObject; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 2) +@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +public class ChainedInstrumentationBenchmark { + + @Param({"0", "1", "10"}) + public int num; + + ChainedInstrumentation chainedInstrumentation; + GraphQLSchema schema; + InstrumentationExecutionParameters parameters; + InstrumentationState instrumentationState; + + @Setup(Level.Trial) + public void setUp() throws ExecutionException, InterruptedException { + GraphQLObjectType queryType = newObject() + .name("benchmarkQuery") + .field(newFieldDefinition() + .type(GraphQLString) + .name("benchmark")) + .build(); + schema = GraphQLSchema.newSchema() + .query(queryType) + .build(); + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("benchmark").build(); + InstrumentationCreateStateParameters createStateParameters = new InstrumentationCreateStateParameters(schema, executionInput); + + List instrumentations = Collections.nCopies(num, new SimplePerformantInstrumentation()); + chainedInstrumentation = new ChainedInstrumentation(instrumentations); + instrumentationState = chainedInstrumentation.createStateAsync(createStateParameters).get(); + parameters = new InstrumentationExecutionParameters(executionInput, schema, instrumentationState); + } + + @Benchmark + public GraphQLSchema benchmarkInstrumentSchema() { + return chainedInstrumentation.instrumentSchema(schema, parameters, instrumentationState); + } + + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include("benchmark.ChainedInstrumentationBenchmark") + .forks(1) + .build(); + + new Runner(opt).run(); + } + +} From 4c8f51716d08a940c7c30a4c63d7932b2d1d16d1 Mon Sep 17 00:00:00 2001 From: Danny Thomas Date: Tue, 20 Feb 2024 17:25:37 +1100 Subject: [PATCH 229/393] Add instrumentation with null state --- .../instrumentation/ChainedInstrumentationStateTest.groovy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy index 9328b5a6c6..135e1e5276 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy @@ -21,9 +21,12 @@ class ChainedInstrumentationStateTest extends Specification { def a = new NamedInstrumentation("A") def b = new NamedInstrumentation("B") def c = new NamedInstrumentation("C") + def nullState = new SimplePerformantInstrumentation() + def chainedInstrumentation = new ChainedInstrumentation([ a, b, + nullState, c, ]) From d26ed000666f698fbc6cda29bbfcddeb806c3a67 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 20 Feb 2024 22:19:59 +1100 Subject: [PATCH 230/393] Small code tweaks on recent PR --- .../instrumentation/ChainedInstrumentation.java | 16 ++++++++-------- .../ChainedInstrumentationStateTest.groovy | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index b1b5eb2dbb..d0a59dcebe 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -90,10 +90,11 @@ private T chainedInstrument(InstrumentationState state, T input, ChainedInst } protected ImmutableList chainedMapAndDropNulls(InstrumentationState state, BiFunction mapper) { + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; ImmutableList.Builder result = ImmutableList.builderWithExpectedSize(instrumentations.size()); for (int i = 0; i < instrumentations.size(); i++) { Instrumentation instrumentation = instrumentations.get(i); - InstrumentationState specificState = ((ChainedInstrumentationState) state).getState(i); + InstrumentationState specificState = chainedInstrumentationState.getState(i); T value = mapper.apply(instrumentation, specificState); if (value != null) { result.add(value); @@ -102,10 +103,11 @@ protected ImmutableList chainedMapAndDropNulls(InstrumentationState state return result.build(); } - protected void chainedConsume(InstrumentationState state, BiConsumer stateConsumer) { + protected void chainedConsume(InstrumentationState state, BiConsumer stateConsumer) { + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; for (int i = 0; i < instrumentations.size(); i++) { Instrumentation instrumentation = instrumentations.get(i); - InstrumentationState specificState = ((ChainedInstrumentationState) state).getState(i); + InstrumentationState specificState = chainedInstrumentationState.getState(i); stateConsumer.accept(instrumentation, specificState); } } @@ -358,7 +360,7 @@ public CompletableFuture instrumentExecutionResult(ExecutionRes CompletableFuture> resultsFuture = Async.eachSequentially(entries, (entry, prevResults) -> { Instrumentation instrumentation = entry.getKey(); InstrumentationState specificState = entry.getValue(); - ExecutionResult lastResult = prevResults.size() > 0 ? prevResults.get(prevResults.size() - 1) : executionResult; + ExecutionResult lastResult = !prevResults.isEmpty() ? prevResults.get(prevResults.size() - 1) : executionResult; return instrumentation.instrumentExecutionResult(lastResult, parameters, specificState); }); return resultsFuture.thenApply((results) -> results.isEmpty() ? executionResult : results.get(results.size() - 1)); @@ -483,10 +485,8 @@ public void onCompleted(Object result, Throwable t) { } @FunctionalInterface - private interface ChainedInstrumentationFunction { - - R apply(I t, S u, A v); - + private interface ChainedInstrumentationFunction { + R apply(I instrumentation, S state, V value); } diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy index 135e1e5276..88d7c86538 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy @@ -90,6 +90,8 @@ class ChainedInstrumentationStateTest extends Specification { then: + chainedInstrumentation.getInstrumentations().size() == 4 + a.executionList == expected b.executionList == expected c.executionList == expected From f52305325593dcec70aba9c4a5717b18b6543fa0 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Tue, 20 Feb 2024 18:37:47 -0800 Subject: [PATCH 231/393] Use String concatenation instead of StringBuilder Change some places to use simple String concatenation instead of a StringBuilder, since JDK9+ has additional optimizations added as part of JEP280. Additionally, switch to the StringJoiner class and String.isBlank method in a few places. --- .../persisted/PersistedQuerySupport.java | 2 +- .../java/graphql/language/AstPrinter.java | 40 ++++++------------- .../graphql/language/PrettyAstPrinter.java | 38 ++++++------------ .../java/graphql/schema/GraphQLTypeUtil.java | 12 ++---- .../java/graphql/schema/idl/TypeUtil.java | 12 ++---- 5 files changed, 32 insertions(+), 72 deletions(-) diff --git a/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java b/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java index 816e033f66..af2edef379 100644 --- a/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java +++ b/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java @@ -45,7 +45,7 @@ public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Functio Object persistedQueryId = queryIdOption.get(); return persistedQueryCache.getPersistedQueryDocument(persistedQueryId, executionInput, (queryText) -> { // we have a miss and they gave us nothing - bah! - if (queryText == null || queryText.trim().length() == 0) { + if (queryText == null || queryText.isBlank()) { throw new PersistedQueryNotFound(persistedQueryId); } // validate the queryText hash before returning to the cache which we assume will set it diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index 5070a4f139..3deff35b25 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -10,6 +10,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.StringJoiner; import static graphql.Assert.assertTrue; import static graphql.util.EscapeUtil.escapeJsonString; @@ -489,7 +490,7 @@ private boolean isEmpty(List list) { } private boolean isEmpty(String s) { - return s == null || s.trim().length() == 0; + return s == null || s.isBlank(); } private List nvl(List list) { @@ -525,7 +526,7 @@ private String value(Value value) { } private String description(Node node) { - Description description = ((AbstractDescribedNode) node).getDescription(); + Description description = ((AbstractDescribedNode) node).getDescription(); if (description == null || description.getContent() == null || compactMode) { return ""; } @@ -577,21 +578,13 @@ private String joinTight(List nodes, String delim, String pr } private String join(List nodes, String delim, String prefix, String suffix) { - StringBuilder joined = new StringBuilder(); - joined.append(prefix); + StringJoiner joiner = new StringJoiner(delim, prefix, suffix); - boolean first = true; for (T node : nodes) { - if (first) { - first = false; - } else { - joined.append(delim); - } - joined.append(this.node(node)); + joiner.add(node(node)); } - joined.append(suffix); - return joined.toString(); + return joiner.toString(); } private String spaced(String... args) { @@ -603,22 +596,15 @@ private String smooshed(String... args) { } private String join(String delim, String... args) { - StringBuilder builder = new StringBuilder(); + StringJoiner joiner = new StringJoiner(delim); - boolean first = true; for (final String arg : args) { - if (isEmpty(arg)) { - continue; - } - if (first) { - first = false; - } else { - builder.append(delim); + if (!isEmpty(arg)) { + joiner.add(arg); } - builder.append(arg); } - return builder.toString(); + return joiner.toString(); } String wrap(String start, String maybeString, String end) { @@ -628,7 +614,7 @@ String wrap(String start, String maybeString, String end) { } return ""; } - return new StringBuilder().append(start).append(maybeString).append(!isEmpty(end) ? end : "").toString(); + return start + maybeString + (!isEmpty(end) ? end : ""); } private String block(List nodes) { @@ -637,7 +623,7 @@ private String block(List nodes) { } if (compactMode) { String joinedNodes = joinTight(nodes, " ", "", ""); - return new StringBuilder().append("{").append(joinedNodes).append("}").toString(); + return "{" + joinedNodes + "}"; } return indent(new StringBuilder().append("{\n").append(join(nodes, "\n"))) + "\n}"; @@ -659,7 +645,7 @@ String wrap(String start, Node maybeNode, String end) { if (maybeNode == null) { return ""; } - return new StringBuilder().append(start).append(node(maybeNode)).append(isEmpty(end) ? "" : end).toString(); + return start + node(maybeNode) + (isEmpty(end) ? "" : end); } /** diff --git a/src/main/java/graphql/language/PrettyAstPrinter.java b/src/main/java/graphql/language/PrettyAstPrinter.java index 02a06bf004..8d29f3864d 100644 --- a/src/main/java/graphql/language/PrettyAstPrinter.java +++ b/src/main/java/graphql/language/PrettyAstPrinter.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.StringJoiner; import java.util.function.Function; import java.util.stream.Collectors; @@ -242,7 +243,7 @@ private boolean isEmpty(List list) { } private boolean isEmpty(String s) { - return s == null || s.trim().length() == 0; + return s == null || s.isBlank(); } private List nvl(List list) { @@ -258,7 +259,7 @@ private String outset(Node node) { } private String description(Node node) { - Description description = ((AbstractDescribedNode) node).getDescription(); + Description description = ((AbstractDescribedNode) node).getDescription(); if (description == null || description.getContent() == null) { return ""; } @@ -304,21 +305,13 @@ private String join(List nodes, String delim) { } private String join(List nodes, String delim, String prefix, String suffix) { - StringBuilder joined = new StringBuilder(); + StringJoiner joiner = new StringJoiner(delim, prefix, suffix); - joined.append(prefix); - boolean first = true; for (T node : nodes) { - if (first) { - first = false; - } else { - joined.append(delim); - } - joined.append(node(node)); + joiner.add(node(node)); } - joined.append(suffix); - return joined.toString(); + return joiner.toString(); } private String node(Node node) { @@ -338,22 +331,15 @@ private Function append(String suffix) { } private String join(String delim, String... args) { - StringBuilder builder = new StringBuilder(); + StringJoiner joiner = new StringJoiner(delim); - boolean first = true; for (final String arg : args) { - if (isEmpty(arg)) { - continue; + if (!isEmpty(arg)) { + joiner.add(arg); } - if (first) { - first = false; - } else { - builder.append(delim); - } - builder.append(arg); } - return builder.toString(); + return joiner.toString(); } private String block(List nodes, Node parentNode, String prefix, String suffix, String separatorMultiline, String separatorSingleLine, String whenEmpty) { @@ -381,8 +367,8 @@ private String block(List nodes, Node parentNode, String pre String blockStart = commentParser.getBeginningOfBlockComment(parentNode, prefix) .map(this::comment) - .map(commentText -> String.format("%s %s\n", prefix, commentText)) - .orElse(String.format("%s%s", prefix, (isMultiline ? "\n" : ""))); + .map(commentText -> prefix + " " + commentText + "\n") + .orElseGet(() -> prefix + (isMultiline ? "\n" : "")); String blockEndComments = comments(commentParser.getEndOfBlockComments(parentNode, suffix), "\n", ""); String blockEnd = (isMultiline ? "\n" : "") + suffix; diff --git a/src/main/java/graphql/schema/GraphQLTypeUtil.java b/src/main/java/graphql/schema/GraphQLTypeUtil.java index 1306ce2448..de5cd5e8b7 100644 --- a/src/main/java/graphql/schema/GraphQLTypeUtil.java +++ b/src/main/java/graphql/schema/GraphQLTypeUtil.java @@ -27,18 +27,12 @@ public class GraphQLTypeUtil { */ public static String simplePrint(GraphQLType type) { Assert.assertNotNull(type, () -> "type can't be null"); - StringBuilder sb = new StringBuilder(); if (isNonNull(type)) { - sb.append(simplePrint(unwrapOne(type))); - sb.append("!"); + return simplePrint(unwrapOne(type)) + "!"; } else if (isList(type)) { - sb.append("["); - sb.append(simplePrint(unwrapOne(type))); - sb.append("]"); - } else { - sb.append(((GraphQLNamedType) type).getName()); + return "[" + simplePrint(unwrapOne(type)) + "]"; } - return sb.toString(); + return ((GraphQLNamedType) type).getName(); } public static String simplePrint(GraphQLSchemaElement schemaElement) { diff --git a/src/main/java/graphql/schema/idl/TypeUtil.java b/src/main/java/graphql/schema/idl/TypeUtil.java index e790cf4693..0189666bd4 100644 --- a/src/main/java/graphql/schema/idl/TypeUtil.java +++ b/src/main/java/graphql/schema/idl/TypeUtil.java @@ -17,18 +17,12 @@ public class TypeUtil { * @return the type in graphql SDL format, eg [typeName!]! */ public static String simplePrint(Type type) { - StringBuilder sb = new StringBuilder(); if (isNonNull(type)) { - sb.append(simplePrint(unwrapOne(type))); - sb.append("!"); + return simplePrint(unwrapOne(type)) + "!"; } else if (isList(type)) { - sb.append("["); - sb.append(simplePrint(unwrapOne(type))); - sb.append("]"); - } else { - sb.append(((TypeName) type).getName()); + return "[" + simplePrint(unwrapOne(type)) + "]"; } - return sb.toString(); + return ((TypeName) type).getName(); } /** From 2d41e5369c0a0a23a0a26897478528a60a065ddc Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 21 Feb 2024 17:17:01 +1100 Subject: [PATCH 232/393] Don't build a builder object and then turn it into the actual object --- .../graphql/execution/ExecutionStrategy.java | 32 ++++------ .../java/graphql/execution/FetchedValue.java | 62 +------------------ .../graphql/execution/FieldValueInfo.java | 44 +++---------- .../execution/FieldValueInfoTest.groovy | 18 +++--- 4 files changed, 30 insertions(+), 126 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 0d79d326b8..ce946e38b2 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -475,18 +475,11 @@ protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext execution // if the field returns nothing then they get the context of their parent field localContext = parameters.getLocalContext(); } - return FetchedValue.newFetchedValue() - .fetchedValue(executionContext.getValueUnboxer().unbox(dataFetcherResult.getData())) - .rawFetchedValue(dataFetcherResult.getData()) - .errors(dataFetcherResult.getErrors()) - .localContext(localContext) - .build(); + Object unBoxedValue = executionContext.getValueUnboxer().unbox(dataFetcherResult.getData()); + return new FetchedValue(unBoxedValue, dataFetcherResult.getErrors(), localContext); } else { - return FetchedValue.newFetchedValue() - .fetchedValue(executionContext.getValueUnboxer().unbox(result)) - .rawFetchedValue(result) - .localContext(parameters.getLocalContext()) - .build(); + Object unBoxedValue = executionContext.getValueUnboxer().unbox(result); + return new FetchedValue(unBoxedValue, ImmutableList.of(), parameters.getLocalContext()); } } @@ -608,10 +601,10 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut return completeValueForList(executionContext, parameters, result); } else if (isScalar(fieldType)) { fieldValue = completeValueForScalar(executionContext, parameters, (GraphQLScalarType) fieldType, result); - return FieldValueInfo.newFieldValueInfo(SCALAR).fieldValue(fieldValue).build(); + return new FieldValueInfo(SCALAR, fieldValue); } else if (isEnum(fieldType)) { fieldValue = completeValueForEnum(executionContext, parameters, (GraphQLEnumType) fieldType, result); - return FieldValueInfo.newFieldValueInfo(ENUM).fieldValue(fieldValue).build(); + return new FieldValueInfo(ENUM, fieldValue); } // when we are here, we have a complex type: Interface, Union or Object @@ -628,7 +621,7 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut // complete field as null, validating it is nullable return getFieldValueInfoForNull(parameters); } - return FieldValueInfo.newFieldValueInfo(OBJECT).fieldValue(fieldValue).build(); + return new FieldValueInfo(OBJECT, fieldValue); } private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStrategyParameters parameters, UnresolvedTypeException e) { @@ -649,7 +642,7 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra */ private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters parameters) { CompletableFuture fieldValue = completeValueForNull(parameters); - return FieldValueInfo.newFieldValueInfo(NULL).fieldValue(fieldValue).build(); + return new FieldValueInfo(NULL, fieldValue); } protected CompletableFuture completeValueForNull(ExecutionStrategyParameters parameters) { @@ -674,10 +667,10 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, try { resultIterable = parameters.getNonNullFieldValidator().validate(parameters.getPath(), resultIterable); } catch (NonNullableFieldWasNullException e) { - return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(exceptionallyCompletedFuture(e)).build(); + return new FieldValueInfo(LIST, exceptionallyCompletedFuture(e)); } if (resultIterable == null) { - return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(completedFuture(null)).build(); + return new FieldValueInfo(LIST, completedFuture(null)); } return completeValueForList(executionContext, parameters, resultIterable); } @@ -742,10 +735,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, overallResult.complete(completedResults); }); - return FieldValueInfo.newFieldValueInfo(LIST) - .fieldValue(overallResult) - .fieldValueInfos(fieldValueInfos) - .build(); + return new FieldValueInfo(LIST, overallResult, fieldValueInfos); } protected void handleValueException(CompletableFuture overallResult, Throwable e, ExecutionContext executionContext) { diff --git a/src/main/java/graphql/execution/FetchedValue.java b/src/main/java/graphql/execution/FetchedValue.java index 28d2ce6da6..8ebac38ced 100644 --- a/src/main/java/graphql/execution/FetchedValue.java +++ b/src/main/java/graphql/execution/FetchedValue.java @@ -6,7 +6,6 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import java.util.List; -import java.util.function.Consumer; /** * Note: This is returned by {@link InstrumentationFieldCompleteParameters#getFetchedValue()} @@ -15,14 +14,12 @@ @PublicApi public class FetchedValue { private final Object fetchedValue; - private final Object rawFetchedValue; private final Object localContext; private final ImmutableList errors; - private FetchedValue(Object fetchedValue, Object rawFetchedValue, ImmutableList errors, Object localContext) { + public FetchedValue(Object fetchedValue, List errors, Object localContext) { this.fetchedValue = fetchedValue; - this.rawFetchedValue = rawFetchedValue; - this.errors = errors; + this.errors = ImmutableList.copyOf(errors); this.localContext = localContext; } @@ -33,10 +30,6 @@ public Object getFetchedValue() { return fetchedValue; } - public Object getRawFetchedValue() { - return rawFetchedValue; - } - public List getErrors() { return errors; } @@ -45,64 +38,13 @@ public Object getLocalContext() { return localContext; } - public FetchedValue transform(Consumer builderConsumer) { - Builder builder = newFetchedValue(this); - builderConsumer.accept(builder); - return builder.build(); - } - @Override public String toString() { return "FetchedValue{" + "fetchedValue=" + fetchedValue + - ", rawFetchedValue=" + rawFetchedValue + ", localContext=" + localContext + ", errors=" + errors + '}'; } - public static Builder newFetchedValue() { - return new Builder(); - } - - public static Builder newFetchedValue(FetchedValue otherValue) { - return new Builder() - .fetchedValue(otherValue.getFetchedValue()) - .rawFetchedValue(otherValue.getRawFetchedValue()) - .errors(otherValue.getErrors()) - .localContext(otherValue.getLocalContext()) - ; - } - - public static class Builder { - - private Object fetchedValue; - private Object rawFetchedValue; - private Object localContext; - private ImmutableList errors = ImmutableList.of(); - - public Builder fetchedValue(Object fetchedValue) { - this.fetchedValue = fetchedValue; - return this; - } - - public Builder rawFetchedValue(Object rawFetchedValue) { - this.rawFetchedValue = rawFetchedValue; - return this; - } - - public Builder localContext(Object localContext) { - this.localContext = localContext; - return this; - } - - public Builder errors(List errors) { - this.errors = ImmutableList.copyOf(errors); - return this; - } - - public FetchedValue build() { - return new FetchedValue(fetchedValue, rawFetchedValue, errors, localContext); - } - } } \ No newline at end of file diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index 571bbaed1a..c13e915936 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -1,10 +1,10 @@ package graphql.execution; +import com.google.common.collect.ImmutableList; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.PublicApi; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -26,7 +26,11 @@ public enum CompleteValueType { private final CompletableFuture fieldValue; private final List fieldValueInfos; - private FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { + FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue) { + this(completeValueType, fieldValue, ImmutableList.of()); + } + + FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValue = fieldValue; @@ -37,10 +41,11 @@ public CompleteValueType getCompleteValueType() { return completeValueType; } - @Deprecated(since="2023-09-11" ) + @Deprecated(since = "2023-09-11") public CompletableFuture getFieldValue() { return fieldValue.thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()); } + public CompletableFuture getFieldValueFuture() { return fieldValue; } @@ -49,9 +54,6 @@ public List getFieldValueInfos() { return fieldValueInfos; } - public static Builder newFieldValueInfo(CompleteValueType completeValueType) { - return new Builder(completeValueType); - } @Override public String toString() { @@ -62,34 +64,4 @@ public String toString() { '}'; } - @SuppressWarnings("unused") - public static class Builder { - private CompleteValueType completeValueType; - private CompletableFuture fieldValueFuture; - private List listInfos = new ArrayList<>(); - - public Builder(CompleteValueType completeValueType) { - this.completeValueType = completeValueType; - } - - public Builder completeValueType(CompleteValueType completeValueType) { - this.completeValueType = completeValueType; - return this; - } - - public Builder fieldValue(CompletableFuture executionResultFuture) { - this.fieldValueFuture = executionResultFuture; - return this; - } - - public Builder fieldValueInfos(List listInfos) { - assertNotNull(listInfos, () -> "fieldValueInfos can't be null"); - this.listInfos = listInfos; - return this; - } - - public FieldValueInfo build() { - return new FieldValueInfo(completeValueType, fieldValueFuture, listInfos); - } - } } \ No newline at end of file diff --git a/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy b/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy index f34666fa22..5720d28444 100644 --- a/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy +++ b/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy @@ -3,26 +3,26 @@ package graphql.execution import graphql.AssertException import spock.lang.Specification +import java.util.concurrent.CompletableFuture -class FieldValueInfoTest extends Specification{ +import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR + + +class FieldValueInfoTest extends Specification { def "simple constructor test"() { when: - def fieldValueInfo = FieldValueInfo.newFieldValueInfo().build() + def fieldValueInfo = new FieldValueInfo(SCALAR, CompletableFuture.completedFuture("A")) then: "fieldValueInfos to be empty list" fieldValueInfo.fieldValueInfos == [] as List - - and: "other fields to be null " - fieldValueInfo.fieldValueFuture == null - fieldValueInfo.completeValueType == null + fieldValueInfo.fieldValueFuture.join() == "A" + fieldValueInfo.completeValueType == SCALAR } def "negative constructor test"() { when: - FieldValueInfo.newFieldValueInfo() - .fieldValueInfos(null) - .build() + new FieldValueInfo(SCALAR, CompletableFuture.completedFuture("A"), null) then: def assEx = thrown(AssertException) assEx.message.contains("fieldValueInfos") From 5d99d65307f5fd4f93f20272f2e42c1759a925a2 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 21 Feb 2024 17:20:10 +1100 Subject: [PATCH 233/393] Don't build a builder object and then turn it into the actual object - package level --- src/main/java/graphql/execution/FetchedValue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/FetchedValue.java b/src/main/java/graphql/execution/FetchedValue.java index 8ebac38ced..0a643f8b71 100644 --- a/src/main/java/graphql/execution/FetchedValue.java +++ b/src/main/java/graphql/execution/FetchedValue.java @@ -17,7 +17,7 @@ public class FetchedValue { private final Object localContext; private final ImmutableList errors; - public FetchedValue(Object fetchedValue, List errors, Object localContext) { + FetchedValue(Object fetchedValue, List errors, Object localContext) { this.fetchedValue = fetchedValue; this.errors = ImmutableList.copyOf(errors); this.localContext = localContext; From a4202708df4cde39b15cb6597ad5c66835e991f5 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 21 Feb 2024 17:49:09 +1100 Subject: [PATCH 234/393] merged master --- .../execution/instrumentation/ChainedInstrumentation.java | 1 - src/test/java/benchmark/ChainedInstrumentationBenchmark.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 9f9b0b206a..6f9d7d2aff 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -43,7 +43,6 @@ * * @see graphql.execution.instrumentation.Instrumentation */ -@SuppressWarnings("deprecation") @PublicApi public class ChainedInstrumentation implements Instrumentation { diff --git a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java index 129d266765..b4b3bcd919 100644 --- a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java +++ b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java @@ -57,7 +57,7 @@ public void setUp() throws ExecutionException, InterruptedException { List instrumentations = Collections.nCopies(num, new SimplePerformantInstrumentation()); chainedInstrumentation = new ChainedInstrumentation(instrumentations); instrumentationState = chainedInstrumentation.createStateAsync(createStateParameters).get(); - parameters = new InstrumentationExecutionParameters(executionInput, schema, instrumentationState); + parameters = new InstrumentationExecutionParameters(executionInput, schema); } @Benchmark From 0bc0845f18c33af5859288ce15244e804a92fc2c Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 22 Feb 2024 14:23:19 +1000 Subject: [PATCH 235/393] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5a68fa5ba7..4d692c6973 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ This is a [GraphQL](https://github.com/graphql/graphql-spec) Java implementation ### Documentation +The GraphQL Java book, from the maintainers: [GraphQL with Java and Spring](https://leanpub.com/graphql-java/) + See our tutorial for beginners: [Getting started with GraphQL Java and Spring Boot](https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/) For further details, please see the documentation: https://www.graphql-java.com/documentation/getting-started From 7a4cccfe24c7a0746b49c6a36a88c1a5c417106e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 22 Feb 2024 20:56:19 +1100 Subject: [PATCH 236/393] More polymorphic behavior --- src/main/java/graphql/execution/Async.java | 77 +++++++++++----- .../groovy/graphql/execution/AsyncTest.groovy | 89 +++++++++++++++++++ 2 files changed, 142 insertions(+), 24 deletions(-) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index 576f2f5cf0..b186b6c8c3 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -246,50 +246,79 @@ private CompletableFuture[] copyOnlyCFsToArray() { } - public static CompletableFuture> each(Collection list, Function> cfFactory) { - CombinedBuilder futures = ofExpectedSize(list.size()); + @SuppressWarnings("unchecked") + public static CompletableFuture> each(Collection list, Function cfOrMaterialisedValueFactory) { + Object l = eachPolymorphic(list, cfOrMaterialisedValueFactory); + if (l instanceof CompletableFuture) { + return (CompletableFuture>) l; + } else { + return CompletableFuture.completedFuture((List) l); + } + } + + /** + * This will run the value factory for each of the values in the provided list. + *

+ * If any of the values provided is a {@link CompletableFuture} it will return a {@link CompletableFuture} result object + * that joins on all values otherwise if none of the values are a {@link CompletableFuture} then it will return a materialized list. + * + * @param list the list to work over + * @param cfOrMaterialisedValueFactory the value factory to call for each iterm in the list + * @param for two + * + * @return a {@link CompletableFuture} to the list of resolved values or the list of values in a materialized fashion + */ + public static /* CompletableFuture> | List */ Object eachPolymorphic(Collection list, Function cfOrMaterialisedValueFactory) { + CombinedBuilder futures = ofExpectedSize(list.size()); for (T t : list) { - CompletableFuture cf; try { - cf = cfFactory.apply(t); - Assert.assertNotNull(cf, () -> "cfFactory must return a non null value"); + Object value = cfOrMaterialisedValueFactory.apply(t); + Assert.assertNotNull(value, () -> "cfOrMaterialisedValueFactory must return a non null value"); + futures.addObject(value); } catch (Exception e) { - cf = new CompletableFuture<>(); + CompletableFuture cf = new CompletableFuture<>(); // Async.each makes sure that it is not a CompletionException inside a CompletionException cf.completeExceptionally(new CompletionException(e)); + futures.add(cf); } - futures.add(cf); } - return futures.await(); + return futures.awaitPolymorphic(); } - public static CompletableFuture> eachSequentially(Iterable list, BiFunction, CompletableFuture> cfFactory) { + public static CompletableFuture> eachSequentially(Iterable list, BiFunction, Object> cfOrMaterialisedValueFactory) { CompletableFuture> result = new CompletableFuture<>(); - eachSequentiallyImpl(list.iterator(), cfFactory, new ArrayList<>(), result); + eachSequentiallyPolymorphicImpl(list.iterator(), cfOrMaterialisedValueFactory, new ArrayList<>(), result); return result; } - private static void eachSequentiallyImpl(Iterator iterator, BiFunction, CompletableFuture> cfFactory, List tmpResult, CompletableFuture> overallResult) { + @SuppressWarnings("unchecked") + private static void eachSequentiallyPolymorphicImpl(Iterator iterator, BiFunction, Object> cfOrMaterialisedValueFactory, List tmpResult, CompletableFuture> overallResult) { if (!iterator.hasNext()) { overallResult.complete(tmpResult); return; } - CompletableFuture cf; + Object value; try { - cf = cfFactory.apply(iterator.next(), tmpResult); - Assert.assertNotNull(cf, () -> "cfFactory must return a non null value"); + value = cfOrMaterialisedValueFactory.apply(iterator.next(), tmpResult); + Assert.assertNotNull(value, () -> "cfOrMaterialisedValueFactory must return a non null value"); } catch (Exception e) { - cf = new CompletableFuture<>(); - cf.completeExceptionally(new CompletionException(e)); + overallResult.completeExceptionally(new CompletionException(e)); + return; + } + if (value instanceof CompletableFuture) { + CompletableFuture cf = (CompletableFuture) value; + cf.whenComplete((cfResult, exception) -> { + if (exception != null) { + overallResult.completeExceptionally(exception); + return; + } + tmpResult.add(cfResult); + eachSequentiallyPolymorphicImpl(iterator, cfOrMaterialisedValueFactory, tmpResult, overallResult); + }); + } else { + tmpResult.add((U) value); + eachSequentiallyPolymorphicImpl(iterator, cfOrMaterialisedValueFactory, tmpResult, overallResult); } - cf.whenComplete((cfResult, exception) -> { - if (exception != null) { - overallResult.completeExceptionally(exception); - return; - } - tmpResult.add(cfResult); - eachSequentiallyImpl(iterator, cfFactory, tmpResult, overallResult); - }); } diff --git a/src/test/groovy/graphql/execution/AsyncTest.groovy b/src/test/groovy/graphql/execution/AsyncTest.groovy index 0b58d40684..34929b0bff 100644 --- a/src/test/groovy/graphql/execution/AsyncTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncTest.groovy @@ -1,3 +1,4 @@ +//file:noinspection GroovyVariableNotAssigned package graphql.execution import spock.lang.Specification @@ -48,6 +49,42 @@ class AsyncTest extends Specification { result.get() == ['x', 'y', 'z'] } + def "eachSequentially polymorphic test"() { + given: + def input = ['a', 'b', 'c'] + def cfFactory = Mock(BiFunction) + def cf1 = new CompletableFuture() + def v2 = 'y' + def cf3 = new CompletableFuture() + + when: + def result = Async.eachSequentially(input, cfFactory) + + then: + !result.isDone() + 1 * cfFactory.apply('a', []) >> cf1 + + when: + cf1.complete('x') + + then: + !result.isDone() + 1 * cfFactory.apply('b', ['x']) >> v2 + + when: + + then: + !result.isDone() + 1 * cfFactory.apply('c', ['x', 'y']) >> cf3 + + when: + cf3.complete('z') + + then: + result.isDone() + result.get() == ['x', 'y', 'z'] + } + def "eachSequentially propagates exception"() { given: def input = ['a', 'b', 'c'] @@ -109,6 +146,58 @@ class AsyncTest extends Specification { result.get() == ['x', 'y', 'z'] } + def "each works for mapping function with polymorphic values"() { + given: + def input = ['a', 'b', 'c'] + def cfFactory = Mock(Function) + cfFactory.apply('a') >> completedFuture('x') + cfFactory.apply('b') >> 'y' + cfFactory.apply('c') >> completedFuture('z') + + + when: + def result = Async.each(input, cfFactory) + + then: + result.isDone() + result.get() == ['x', 'y', 'z'] + } + + def "eachPolymorphic works for mapping function with polymorphic values"() { + given: + def input = ['a', 'b', 'c'] + def cfFactory = Mock(Function) + cfFactory.apply('a') >> completedFuture('x') + cfFactory.apply('b') >> 'y' + cfFactory.apply('c') >> completedFuture('z') + + + when: + def result = Async.eachPolymorphic(input, cfFactory) + + then: + result instanceof CompletableFuture + (result as CompletableFuture).isDone() + (result as CompletableFuture).get() == ['x', 'y', 'z'] + } + + def "eachPolymorphic works for mapping function with materialised values"() { + given: + def input = ['a', 'b', 'c'] + def cfFactory = Mock(Function) + cfFactory.apply('a') >> 'x' + cfFactory.apply('b') >> 'y' + cfFactory.apply('c') >> 'z' + + + when: + def result = Async.eachPolymorphic(input, cfFactory) + + then: + result instanceof List + result == ['x', 'y', 'z'] + } + def "each with mapping function propagates factory exception"() { given: def input = ['a', 'b', 'c'] From 55902abdbe93ebe3d06df9c7bebb011d245f1581 Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Thu, 22 Feb 2024 23:10:02 +1000 Subject: [PATCH 237/393] Update defer validation - added more friendly i18n error messages, added further tests and fixed defer label, on root level and vali operation rules. --- .../java/graphql/validation/AbstractRule.java | 12 + .../validation/ValidationErrorType.java | 2 + .../java/graphql/validation/Validator.java | 7 +- .../validation/rules/DeferDirectiveLabel.java | 51 +++- .../rules/DeferDirectiveOnRootLevel.java | 62 ++--- .../rules/DeferDirectiveOnValidOperation.java | 46 +++- src/main/resources/i18n/Validation.properties | 12 +- ....groovy => DeferDirectiveLabelTest.groovy} | 91 ++++++- .../DeferDirectiveOnRootLevelTest.groovy | 237 +++++++++++------- .../DeferDirectiveOnValidOperationTest.groovy | 161 ++++++++++-- 10 files changed, 488 insertions(+), 193 deletions(-) rename src/test/groovy/graphql/validation/rules/{DeferDirectivesTest.groovy => DeferDirectiveLabelTest.groovy} (56%) diff --git a/src/main/java/graphql/validation/AbstractRule.java b/src/main/java/graphql/validation/AbstractRule.java index c5c2f5a56a..0cc31edc75 100644 --- a/src/main/java/graphql/validation/AbstractRule.java +++ b/src/main/java/graphql/validation/AbstractRule.java @@ -1,6 +1,7 @@ package graphql.validation; +import graphql.ExperimentalApi; import graphql.Internal; import graphql.i18n.I18nMsg; import graphql.language.Argument; @@ -90,6 +91,17 @@ protected List getQueryPath() { return validationContext.getQueryPath(); } + /** + * Verifies if the experimental API key is enabled + * @param key to be checked + * @return if the experimental API key is enabled + */ + protected Boolean isExperimentalApiKeyEnabled(String key) { + return (getValidationContext() != null && + getValidationContext().getGraphQLContext() != null || + getValidationContext().getGraphQLContext().get(key) != null || + ((Boolean) getValidationContext().getGraphQLContext().get(key))); + } /** * Creates an I18n message using the {@link graphql.i18n.I18nMsg} * diff --git a/src/main/java/graphql/validation/ValidationErrorType.java b/src/main/java/graphql/validation/ValidationErrorType.java index 5ae5be0aaf..5710a1b0b9 100644 --- a/src/main/java/graphql/validation/ValidationErrorType.java +++ b/src/main/java/graphql/validation/ValidationErrorType.java @@ -27,6 +27,7 @@ public enum ValidationErrorType implements ValidationErrorClassification { UnknownDirective, MisplacedDirective, UndefinedVariable, + VariableNotAllowed, UnusedVariable, FragmentCycle, FieldsConflict, @@ -37,6 +38,7 @@ public enum ValidationErrorType implements ValidationErrorClassification { DuplicateFragmentName, DuplicateDirectiveName, DuplicateArgumentNames, + DuplicateIncrementalLabel, DuplicateVariableName, NullValueForNonNullArgument, SubscriptionMultipleRootFields, diff --git a/src/main/java/graphql/validation/Validator.java b/src/main/java/graphql/validation/Validator.java index cd47ed2fe4..d7c3db2fdc 100644 --- a/src/main/java/graphql/validation/Validator.java +++ b/src/main/java/graphql/validation/Validator.java @@ -1,6 +1,7 @@ package graphql.validation; +import graphql.ExperimentalApi; import graphql.Internal; import graphql.i18n.I18n; import graphql.language.Document; @@ -30,7 +31,6 @@ import graphql.validation.rules.UniqueArgumentNames; import graphql.validation.rules.UniqueDirectiveNamesPerLocation; import graphql.validation.rules.UniqueFragmentNames; -import graphql.validation.rules.UniqueObjectFieldName; import graphql.validation.rules.UniqueOperationNames; import graphql.validation.rules.UniqueVariableNames; import graphql.validation.rules.VariableDefaultValuesOfCorrectType; @@ -161,15 +161,14 @@ public List createRules(ValidationContext validationContext, Valid UniqueObjectFieldName uniqueObjectFieldName = new UniqueObjectFieldName(validationContext, validationErrorCollector); rules.add(uniqueObjectFieldName); - DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector); - rules.add(deferDirectiveLabel); - DeferDirectiveOnRootLevel deferDirectiveOnRootLevel = new DeferDirectiveOnRootLevel(validationContext, validationErrorCollector); rules.add(deferDirectiveOnRootLevel); DeferDirectiveOnValidOperation deferDirectiveOnValidOperation = new DeferDirectiveOnValidOperation(validationContext, validationErrorCollector); rules.add(deferDirectiveOnValidOperation); + DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector); + rules.add(deferDirectiveLabel); return rules; } } diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java index 37ab679945..03a962d3d6 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java @@ -1,41 +1,68 @@ package graphql.validation.rules; -import graphql.Internal; +import graphql.Directives; +import graphql.ExperimentalApi; import graphql.language.Argument; import graphql.language.Directive; import graphql.language.Node; +import graphql.language.NullValue; import graphql.language.StringValue; +import graphql.language.Value; import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; + import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static graphql.validation.ValidationErrorType.DuplicateArgumentNames; +import static graphql.validation.ValidationErrorType.DuplicateIncrementalLabel; +import static graphql.validation.ValidationErrorType.VariableNotAllowed; +import static graphql.validation.ValidationErrorType.WrongType; -@Internal +/** + * Defer and stream directive labels are unique + * + * A GraphQL document is only valid if defer and stream directives' label argument is static and unique. + * + * See proposed spec:spec/Section 5 -- Validation.md ### ### Defer And Stream Directive Labels Are Unique + */ +@ExperimentalApi public class DeferDirectiveLabel extends AbstractRule { - private Set labels = new LinkedHashSet<>(); + private Set checkedLabels = new LinkedHashSet<>(); public DeferDirectiveLabel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { super(validationContext, validationErrorCollector); } @Override public void checkDirective(Directive directive, List ancestors) { - if (!directive.getName().equals("defer") || directive.getArguments().size() == 0) { + // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true + if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT) || + !Directives.DeferDirective.getName().equals(directive.getName()) || + directive.getArguments().size() == 0) { return; } + Argument labelArgument = directive.getArgument("label"); - // argument type is validated in DeferDirectiveArgumentType - if (labelArgument != null && labelArgument.getValue() instanceof StringValue) { - if (labels.contains(((StringValue) labelArgument.getValue()).getValue())) { - String message = i18n(DuplicateArgumentNames, "UniqueArgumentNames.directiveUniqueArgument", labelArgument.getName(), directive.getName()); - addError(DuplicateArgumentNames, directive.getSourceLocation(), message); - } else { - labels.add(((StringValue) labelArgument.getValue()).getValue() ); - } + if (labelArgument == null || labelArgument.getValue() instanceof NullValue){ + return; + } + Value labelArgumentValue = labelArgument.getValue(); + + if (!(labelArgumentValue instanceof StringValue)) { + String message = i18n(WrongType, "DeferDirective.labelMustBeStaticString"); + addError(WrongType, directive.getSourceLocation(), message); + } else { + if (checkedLabels.contains(((StringValue) labelArgumentValue).getValue())) { + String message = i18n(DuplicateIncrementalLabel, "IncrementalDirective.uniqueArgument", labelArgument.getName(), directive.getName()); + addError(DuplicateIncrementalLabel, directive.getSourceLocation(), message); + } else { + checkedLabels.add(((StringValue) labelArgumentValue).getValue()); + } } } + + } \ No newline at end of file diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java index c4544d8f17..5b907bf29a 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java @@ -1,24 +1,34 @@ package graphql.validation.rules; +import graphql.Directives; +import graphql.ExperimentalApi; import graphql.language.Directive; -import graphql.language.FragmentDefinition; -import graphql.language.InlineFragment; import graphql.language.Node; import graphql.language.OperationDefinition; -import graphql.language.SelectionSet; +import graphql.schema.GraphQLCompositeType; +import graphql.schema.GraphQLObjectType; import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; -import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import static graphql.validation.ValidationErrorType.MisplacedDirective; +/** + * Defer and stream directives are used on valid root field + * + * A GraphQL document is only valid if defer directives are not used on root mutation or subscription types. + * + * See proposed spec:spec/Section 5 -- Validation.md ### Defer And Stream Directives Are Used On Valid Root Field + */ +@ExperimentalApi public class DeferDirectiveOnRootLevel extends AbstractRule { - + private Set invalidOperations = new LinkedHashSet(Arrays.asList(OperationDefinition.Operation.MUTATION, OperationDefinition.Operation.SUBSCRIPTION)); public DeferDirectiveOnRootLevel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { super(validationContext, validationErrorCollector); this.setVisitFragmentSpreads(true); @@ -26,32 +36,24 @@ public DeferDirectiveOnRootLevel(ValidationContext validationContext, Validation @Override public void checkDirective(Directive directive, List ancestors) { - if (directive.getName().equals("defer")){ - Optional fragmentAncestor = getFragmentAncestor(ancestors); - if (fragmentAncestor.isPresent() && fragmentAncestor.get() instanceof OperationDefinition){ - OperationDefinition operationDefinition = (OperationDefinition) fragmentAncestor.get(); - if (OperationDefinition.Operation.MUTATION.equals(operationDefinition.getOperation()) || OperationDefinition.Operation.SUBSCRIPTION.equals(operationDefinition.getOperation())) { - String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), operationDefinition.getOperation().name().toLowerCase()); - addError(MisplacedDirective, directive.getSourceLocation(), message); - } - } + // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true + if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { + return; + } + if (!Directives.DeferDirective.getName().equals(directive.getName())) { + return; + } + GraphQLObjectType mutationType = getValidationContext().getSchema().getMutationType(); + GraphQLObjectType subscriptionType = getValidationContext().getSchema().getSubscriptionType(); + GraphQLCompositeType parentType = getValidationContext().getParentType(); + if (mutationType != null && parentType != null && parentType.getName().equals(mutationType.getName())){ + String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelMutation", parentType.getName()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } else if (subscriptionType != null && parentType != null && parentType.getName().equals(subscriptionType.getName())) { + String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelSubscription", parentType.getName()); + addError(MisplacedDirective, directive.getSourceLocation(), message); } } - /** - * Get the first ancestor that is not InlineFragment, SelectionSet or FragmentDefinition - * @param ancestors list of ancestors - * @return Optional of Node parent that is not InlineFragment, SelectionSet or FragmentDefinition. - */ - protected Optional getFragmentAncestor(List ancestors){ - List ancestorsCopy = new ArrayList(ancestors); - Collections.reverse(ancestorsCopy); - return ancestorsCopy.stream().filter(node -> !( - node instanceof InlineFragment || - node instanceof SelectionSet || - node instanceof FragmentDefinition - ) - ).findFirst(); - } } diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java index f506f1c49b..6baa9df948 100644 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java +++ b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java @@ -1,13 +1,17 @@ package graphql.validation.rules; +import graphql.Directives; +import graphql.ExperimentalApi; +import graphql.language.Argument; import graphql.language.BooleanValue; import graphql.language.Directive; -import graphql.language.Document; import graphql.language.Node; import graphql.language.OperationDefinition; +import graphql.language.VariableReference; import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; + import java.util.List; import java.util.Optional; @@ -18,24 +22,34 @@ * Defer Directive is Used On Valid Operations * * A GraphQL document is only valid if defer directives are not used on subscription types. + * + * See proposed spec:spec/Section 5 -- Validation.md ### Defer And Stream Directives Are Used On Valid Operations + * */ +@ExperimentalApi public class DeferDirectiveOnValidOperation extends AbstractRule { - public DeferDirectiveOnValidOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { super(validationContext, validationErrorCollector); this.setVisitFragmentSpreads(true); } + @Override public void checkDirective(Directive directive, List ancestors) { - if (!directive.getName().equals("defer") || - (directive.getArgumentsByName().get("if") != null && !((BooleanValue) directive.getArgumentsByName().get("if").getValue()).isValue() )) { + // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true + if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { + return; + } + + if (!Directives.DeferDirective.getName().equals(directive.getName())) { return; } // check if the directive is on allowed operation - Optional operationDefinition = getOperation(ancestors); - if (operationDefinition.isPresent() && operationDefinition.get().getOperation() == SUBSCRIPTION) { - String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperation", directive.getName(), SUBSCRIPTION.name().toLowerCase()); + Optional operationDefinition = getOperationDefinition(ancestors); + if (operationDefinition.isPresent() && + SUBSCRIPTION.equals(operationDefinition.get().getOperation()) && + !ifArgumentMightBeFalse(directive) ){ + String message = i18n(MisplacedDirective, "IncrementalDirective.notAllowedSubscriptionOperation", directive.getName()); addError(MisplacedDirective, directive.getSourceLocation(), message); } } @@ -43,14 +57,28 @@ public void checkDirective(Directive directive, List ancestors) { /** * Extract from ancestors the OperationDefinition using the document ancestor. * @param ancestors list of ancestors - * @return OperationDefinition + * @return Optional of OperationDefinition */ - private Optional getOperation(List ancestors) { + private Optional getOperationDefinition(List ancestors) { return ancestors.stream() .filter(doc -> doc instanceof OperationDefinition) .map((def -> (OperationDefinition) def)) .findFirst(); } + private Boolean ifArgumentMightBeFalse(Directive directive) { + Argument ifArgument = directive.getArgumentsByName().get("if"); + if (ifArgument == null) { + return false; + } + if(ifArgument.getValue() instanceof BooleanValue){ + return !((BooleanValue) ifArgument.getValue()).isValue(); + } + if(ifArgument.getValue() instanceof VariableReference){ + return true; + } + return false; + } + } diff --git a/src/main/resources/i18n/Validation.properties b/src/main/resources/i18n/Validation.properties index e5921e9746..c12920da07 100644 --- a/src/main/resources/i18n/Validation.properties +++ b/src/main/resources/i18n/Validation.properties @@ -10,6 +10,14 @@ # REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them # so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' # + +DeferDirective.notAllowedOperationRootLevelMutation=Validation error ({0}) : Defer directive cannot be used on root mutation type ''{1}'' +DeferDirective.notAllowedOperationRootLevelSubscription=Validation error ({0}) : Defer directive cannot be used on root subscription type ''{1}'' +DeferDirective.labelMustBeStaticString= Validation error ({0}) : Defer directive?s label argument must be a static string +IncrementalDirective.notAllowedSubscriptionOperation=Validation error ({0}) : Directive ''{1}'' is not allowed to be used on operation subscription + +IncrementalDirective.uniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}'' for directive defer/Stream +# ExecutableDefinitions.notExecutableType=Validation error ({0}) : Type ''{1}'' definition is not executable ExecutableDefinitions.notExecutableSchema=Validation error ({0}) : Schema definition is not executable ExecutableDefinitions.notExecutableDirective=Validation error ({0}) : Directive ''{1}'' definition is not executable @@ -63,8 +71,6 @@ SubscriptionIntrospectionRootField.introspectionRootField=Validation error ({0}) SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validation error ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' cannot be an introspection field # UniqueArgumentNames.uniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}'' - -UniqueArgumentNames.directiveUniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}'' for directive ''{2}'' # UniqueDirectiveNamesPerLocation.uniqueDirectives=Validation error ({0}) : Non repeatable directives must be uniquely named within a location. The directive ''{1}'' used on a ''{2}'' is not unique # @@ -101,5 +107,3 @@ ArgumentValidationUtil.handleMissingFieldsError=Validation error ({0}) : argumen # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleExtraFieldError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' contains a field not in ''{3}'': ''{4}'' -DirectiveMisplaced.notAllowedOperationRootLevel=Validation error ({0}) : Directive ''{1}'' is not allowed on root of operation ''{2}'' -DirectiveMisplaced.notAllowedOperation=Validation error ({0}) : Directive ''{1}'' is not allowed on operation ''{2}'' \ No newline at end of file diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectivesTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy similarity index 56% rename from src/test/groovy/graphql/validation/rules/DeferDirectivesTest.groovy rename to src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy index 9d88117457..2e3975269d 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectivesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy @@ -1,5 +1,7 @@ package graphql.validation.rules +import graphql.ExperimentalApi +import graphql.GraphQLContext import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal @@ -13,9 +15,10 @@ import graphql.validation.ValidationErrorType import graphql.validation.Validator import spock.lang.Specification -class DeferDirectivesTest extends Specification { +class DeferDirectiveLabelTest extends Specification { ValidationContext validationContext = Mock(ValidationContext) + ValidationErrorCollector errorCollector = new ValidationErrorCollector() DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, errorCollector) @@ -23,6 +26,9 @@ class DeferDirectivesTest extends Specification { def setup() { def traversalContext = Mock(TraversalContext) validationContext.getSchema() >> SpecValidationSchema.specValidationSchema + validationContext.getGraphQLContext() >> GraphQLContext.newContext().of( + ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true + ).build(); validationContext.getTraversalContext() >> traversalContext } @@ -74,7 +80,7 @@ class DeferDirectivesTest extends Specification { then: !errorCollector.errors.isEmpty() - errorCollector.containsValidationError(ValidationErrorType.DuplicateArgumentNames) + errorCollector.containsValidationError(ValidationErrorType.DuplicateIncrementalLabel) } def "Multiple use of Defer directive is valid"() { @@ -91,14 +97,17 @@ class DeferDirectivesTest extends Specification { } } """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + when: - def validationErrors = validate(query) + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) then: - validationErrors.isEmpty() + errorCollector.errors.isEmpty() } - def "Multiple use of Defer directive with different labels is valid"() { + def "Allow Multiple use of Defer directive with different labels"() { given: def query = """ query defer_query { @@ -112,13 +121,41 @@ class DeferDirectivesTest extends Specification { } } """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + + then: + errorCollector.errors.isEmpty() + } + + + def "Label cannot be an argument directive"() { + given: + def query = """ + query defer_query(\$label: Int) { + ... @defer(label:\$label) { + human { + name + } + } + } + """ + + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + when: - def validationErrors = validate(query) + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) then: - validationErrors.isEmpty() + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.WrongType) } + def "Defer directive Label must be string"() { given: def query = """ @@ -130,17 +167,45 @@ class DeferDirectivesTest extends Specification { } } """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + + when: + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + + then: + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.WrongType) + } + + def "defer with null label should behave as if no label was provided"() { + def query = ''' + query { + dog { + ... @defer(label: null) { + name + } + } + cat { + ... @defer(label: null) { + name + } + } + } + ''' + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal() + when: - def validationErrors = validate(query) + languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) then: - !validationErrors.isEmpty() - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'label' with value 'IntValue{value=1}' is not a valid 'String' - Expected an AST type of 'StringValue' but it was a 'IntValue'" + errorCollector.errors.isEmpty() } - static List validate(String query) { + + static List validate(String query) { def document = new Parser().parseDocument(query) return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) } diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy index cce20c3c4d..ec65ec1937 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy @@ -1,39 +1,33 @@ package graphql.validation.rules +import graphql.ExperimentalApi import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal import graphql.validation.RulesVisitor import graphql.validation.SpecValidationSchema -import graphql.validation.TraversalContext import graphql.validation.ValidationContext -import graphql.validation.ValidationError import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType -import graphql.validation.Validator import spock.lang.Specification class DeferDirectiveOnRootLevelTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) ValidationErrorCollector errorCollector = new ValidationErrorCollector() - DeferDirectiveOnRootLevel deferDirectiveOnRootLevel = new DeferDirectiveOnRootLevel(validationContext, errorCollector) - def traverse(String query) { Document document = new Parser().parseDocument(query) - I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) - ValidationContext validationContext = new ValidationContext(SpecValidationSchema.specValidationSchema, document, i18n) + ValidationContext validationContext = new ValidationContext( + SpecValidationSchema.specValidationSchema, + document, + I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH)) + validationContext.getGraphQLContext().put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true) + LanguageTraversal languageTraversal = new LanguageTraversal() languageTraversal.traverse(document, new RulesVisitor(validationContext, [new DeferDirectiveOnRootLevel(validationContext, errorCollector)])) } - def setup() { - def traversalContext = Mock(TraversalContext) - validationContext.getSchema() >> SpecValidationSchema.specValidationSchema - validationContext.getTraversalContext() >> traversalContext - } def "Not allow defer on subscription root level"() { given: @@ -46,11 +40,9 @@ class DeferDirectiveOnRootLevelTest extends Specification { } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + traverse(query) then: !errorCollector.errors.isEmpty() @@ -69,17 +61,16 @@ class DeferDirectiveOnRootLevelTest extends Specification { } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() + when: - def validationErrors = validate(query) + traverse(query) then: - !validationErrors.isEmpty() - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective - validationErrors.get(0).message == "Validation error (MisplacedDirective) : Directive 'defer' is not allowed on root of operation 'mutation'" + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective) : Defer directive cannot be used on root mutation type 'PetMutationType'" } @@ -95,28 +86,7 @@ class DeferDirectiveOnRootLevelTest extends Specification { } """ when: - def validationErrors = validate(query) - - then: - validationErrors.isEmpty() - } - - def "allow defer on when is not on mutation root level"() { - given: - def query = """ - mutation doggo { - createDog(id: "1") { - ... @defer { - id - } - } - } - """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -125,7 +95,6 @@ class DeferDirectiveOnRootLevelTest extends Specification { def "Not allow defer mutation root level on inline fragments "() { given: def query = """ - mutation doggo { ... { ... @defer { @@ -137,18 +106,14 @@ class DeferDirectiveOnRootLevelTest extends Specification { } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - def validationErrors = validate(query) + traverse(query) then: - !validationErrors.isEmpty() - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective - validationErrors.get(0).message == "Validation error (MisplacedDirective) : Directive 'defer' is not allowed on root of operation 'mutation'" - + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective) : Defer directive cannot be used on root mutation type 'PetMutationType'" } def "Not allow defer on subscription root level even when is inside multiple inline fragment"() { @@ -166,15 +131,14 @@ class DeferDirectiveOnRootLevelTest extends Specification { } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + traverse(query) then: !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective) : Defer directive cannot be used on root subscription type 'SubscriptionRoot'" } @@ -200,21 +164,17 @@ class DeferDirectiveOnRootLevelTest extends Specification { """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: traverse(query) then: !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) - + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective@[doggo]) : Defer directive cannot be used on root mutation type 'PetMutationType'" } - - def "Allows defer on mutation when it is not on root level"() { given: def query = """ @@ -230,7 +190,7 @@ class DeferDirectiveOnRootLevelTest extends Specification { LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -241,24 +201,21 @@ class DeferDirectiveOnRootLevelTest extends Specification { def query = """ mutation doggo { ...{ - ...doggoCreate + createDog(id: "1") { + ...doggo + } } } - fragment doggoCreate on PetMutationType { - createDog(id: "1") { - ... @defer { - id - } - } + fragment doggo on Dog { + ... @defer { + id + } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -285,16 +242,12 @@ class DeferDirectiveOnRootLevelTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + traverse(query) then: errorCollector.errors.isEmpty() - } @@ -324,7 +277,7 @@ class DeferDirectiveOnRootLevelTest extends Specification { LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveOnRootLevel])) + traverse(query) then: errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) @@ -359,13 +312,13 @@ class DeferDirectiveOnRootLevelTest extends Specification { """ when: - def validationErrors = validate(query) + traverse(query) then: - !validationErrors.isEmpty() - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective - validationErrors.get(0).message == "Validation error (MisplacedDirective@[createDoggoRoot/createDoggo]) : Directive 'defer' is not allowed on root of operation 'mutation'" + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective@[createDoggoRoot/createDoggo]) : Defer directive cannot be used on root mutation type 'PetMutationType'" } @@ -410,19 +363,111 @@ class DeferDirectiveOnRootLevelTest extends Specification { """ when: - def validationErrors = validate(query) + traverse(query) + + then: + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective@[createDoggoLevel1/createDoggoLevel2/createDoggo]) : Defer directive cannot be used on root mutation type 'PetMutationType'" + + } + + + def "Not allow defer on subscription root level even when defer(if == false) "() { + given: + def query = """ + subscription pets{ + ... @defer(if:false) { + dog { + + name + } + nickname + } + } + """ + Document document = new Parser().parseDocument(query) + LanguageTraversal languageTraversal = new LanguageTraversal()\ + + when: + traverse(query) + + then: + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective) : Defer directive cannot be used on root subscription type 'SubscriptionRoot'" + + } + + def "Not allow defer on subscription root level when defer(if == true) "() { + given: + def query = """ + subscription pets{ + ... @defer(if:true) { + dog { + + name + } + nickname + } + } + """ + + when: + traverse(query) then: - !validationErrors.isEmpty() - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective - validationErrors.get(0).message == "Validation error (MisplacedDirective@[createDoggoLevel1/createDoggoLevel2/createDoggo]) : Directive 'defer' is not allowed on root of operation 'mutation'" + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective) : Defer directive cannot be used on root subscription type 'SubscriptionRoot'" } - static List validate(String query) { - def document = new Parser().parseDocument(query) - return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + def "Not allow defer on mutation root level even when if is variable that could have false as value "() { + given: + def query = """ + mutation pets(\$ifVar:Boolean){ + ... @defer(if:\$ifVar) { + createDog(input: {id: "1"}) { + name + } + } + + } + """ + + when: + traverse(query) + + then: + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective) : Defer directive cannot be used on root mutation type 'PetMutationType'" + } + + def "Not allow defer on mutation root level when defer(if == true) "() { + given: + def query = """ + mutation pets{ + ... @defer(if:true) { + createDog(input: {id: "1"}) { + name + } + } + } + """ + + when: + traverse(query) + + then: + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective) : Defer directive cannot be used on root mutation type 'PetMutationType'" + } + } diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy index 1e6dfa5525..1430b7743b 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy @@ -1,39 +1,68 @@ package graphql.validation.rules +import graphql.ExperimentalApi import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal import graphql.validation.RulesVisitor import graphql.validation.SpecValidationSchema -import graphql.validation.TraversalContext import graphql.validation.ValidationContext -import graphql.validation.ValidationError import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType -import graphql.validation.Validator import spock.lang.Specification class DeferDirectiveOnValidOperationTest extends Specification { - - ValidationContext validationContext = Mock(ValidationContext) ValidationErrorCollector errorCollector = new ValidationErrorCollector() def traverse(String query) { Document document = new Parser().parseDocument(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) ValidationContext validationContext = new ValidationContext(SpecValidationSchema.specValidationSchema, document, i18n) + validationContext.getGraphQLContext().put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true) LanguageTraversal languageTraversal = new LanguageTraversal() languageTraversal.traverse(document, new RulesVisitor(validationContext, [new DeferDirectiveOnValidOperation(validationContext, errorCollector)])) } - def setup() { - def traversalContext = Mock(TraversalContext) - validationContext.getSchema() >> SpecValidationSchema.specValidationSchema - validationContext.getTraversalContext() >> traversalContext + def "Allow simple defer on query with fragment definition"() { + def query = ''' + query { + dog { + ... DogFields @defer + } + } + + fragment DogFields on Dog { + name + } + ''' + + when: + traverse(query) + + then: + errorCollector.errors.isEmpty() } + def "Allow simple defer on mutation with fragment definition"() { + def query = ''' + mutation { + createDog(input: {name: "Fido"}) { + ... DogFields @defer + } + } + + fragment DogFields on Dog { + name + } + ''' + + when: + traverse(query) + then: + errorCollector.errors.isEmpty() + } def "Not allow defer on subscription operation"() { given: @@ -81,6 +110,27 @@ class DeferDirectiveOnValidOperationTest extends Specification { } + def "Not allow simple defer on subscription with fragment definition"() { + def query = ''' + subscription { + dog { + ... DogFields @defer + } + } + + fragment DogFields on Dog { + name + } + ''' + + when: + traverse(query) + + then: + !errorCollector.errors.isEmpty() + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + + } def "Not allow defer on fragment when operation is subscription"() { given: @@ -108,8 +158,6 @@ class DeferDirectiveOnValidOperationTest extends Specification { when: traverse(query) - then: - then: !errorCollector.errors.isEmpty() errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) @@ -185,13 +233,13 @@ class DeferDirectiveOnValidOperationTest extends Specification { """ when: - def validationErrors = validate(query) + traverse(query) then: - !validationErrors.isEmpty() - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective - validationErrors.get(0).message == "Validation error (MisplacedDirective@[doggoSubscription/dog/doggo]) : Directive 'defer' is not allowed on operation 'subscription'" + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.errors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective@[doggoSubscription/dog/doggo]) : Directive 'defer' is not allowed to be used on operation subscription" } @@ -219,13 +267,13 @@ class DeferDirectiveOnValidOperationTest extends Specification { """ when: - def validationErrors = validate(query) + traverse(query) then: - !validationErrors.isEmpty() - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective - validationErrors.get(0).message == "Validation error (MisplacedDirective@[dog]) : Directive 'defer' is not allowed on operation 'subscription'" + !errorCollector.errors.isEmpty() + errorCollector.errors.size() == 1 + errorCollector.errors.get(0).getValidationErrorType() == ValidationErrorType.MisplacedDirective + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective@[dog]) : Directive 'defer' is not allowed to be used on operation subscription" } @@ -247,9 +295,72 @@ class DeferDirectiveOnValidOperationTest extends Specification { errorCollector.errors.isEmpty() } - static List validate(String query) { - def document = new Parser().parseDocument(query) - return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + + def "Allow defer on subscription when defer(if == false) "() { + given: + def query = """ + subscription pets{ + dog { + ... @defer(if:false) { + name + } + nickname + } + } + """ + + when: + traverse(query) + + then: + errorCollector.errors.isEmpty() + + } + + def "Not allow defer on subscription when defer(if == true) "() { + given: + def query = """ + subscription pets{ + dog { + ... @defer(if:true) { + name + } + nickname + } + } + """ + + when: + traverse(query) + + then: + errorCollector.errors.size() == 1 + errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) + errorCollector.errors.get(0).message == "Validation error (MisplacedDirective@[dog]) : Directive 'defer' is not allowed to be used on operation subscription" + + + } + + def "Allow defer when if is variable that could have false as value "() { + given: + def query = """ + subscription pets(\$ifVar:Boolean){ + dog { + ... @defer(if:\$ifVar) { + name + } + nickname + } + } + """ + + when: + traverse(query) + + then: + errorCollector.errors.isEmpty() } -} + + +} \ No newline at end of file From 422a3a9c1f005dcae347768146090472918f8cf9 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 23 Feb 2024 09:50:54 +1100 Subject: [PATCH 238/393] Added an id generator that's more performant --- .../java/graphql/execution/ExecutionId.java | 3 +- .../util/AlternativeJdkIdGenerator.java | 72 +++++++++++++++++++ .../util/AlternativeJdkIdGeneratorTest.groovy | 19 +++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/main/java/graphql/util/AlternativeJdkIdGenerator.java create mode 100644 src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy diff --git a/src/main/java/graphql/execution/ExecutionId.java b/src/main/java/graphql/execution/ExecutionId.java index 2c10a6a719..f36b1bf750 100644 --- a/src/main/java/graphql/execution/ExecutionId.java +++ b/src/main/java/graphql/execution/ExecutionId.java @@ -2,6 +2,7 @@ import graphql.Assert; import graphql.PublicApi; +import graphql.util.AlternativeJdkIdGenerator; import java.util.UUID; @@ -17,7 +18,7 @@ public class ExecutionId { * @return a query execution identifier */ public static ExecutionId generate() { - return new ExecutionId(UUID.randomUUID().toString()); + return new ExecutionId(AlternativeJdkIdGenerator.uuid().toString()); } /** diff --git a/src/main/java/graphql/util/AlternativeJdkIdGenerator.java b/src/main/java/graphql/util/AlternativeJdkIdGenerator.java new file mode 100644 index 0000000000..719aad255d --- /dev/null +++ b/src/main/java/graphql/util/AlternativeJdkIdGenerator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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 + * + * https://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. + */ +/* + * This class was taken from Spring https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/util/AlternativeJdkIdGenerator.java + * as a way to get a more performant UUID generator for use as request ids. SecureRandom can be expensive + * to run on each request as per https://github.com/graphql-java/graphql-java/issues/3435, so this uses SecureRandom + * at application start and then the cheaper Random class each call after that. + */ +package graphql.util; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Random; +import java.util.UUID; + +/** + * An id generator that uses {@link SecureRandom} for the initial seed and + * {@link Random} thereafter. This provides a better balance between securely random ids and performance. + * + * @author Rossen Stoyanchev + * @author Rob Winch + */ +public class AlternativeJdkIdGenerator { + + private static AlternativeJdkIdGenerator idGenerator = new AlternativeJdkIdGenerator(); + + public static UUID uuid() { + return idGenerator.generateId(); + } + + private final Random random; + + + public AlternativeJdkIdGenerator() { + SecureRandom secureRandom = new SecureRandom(); + byte[] seed = new byte[8]; + secureRandom.nextBytes(seed); + this.random = new Random(new BigInteger(seed).longValue()); + } + + + public UUID generateId() { + byte[] randomBytes = new byte[16]; + this.random.nextBytes(randomBytes); + + long mostSigBits = 0; + for (int i = 0; i < 8; i++) { + mostSigBits = (mostSigBits << 8) | (randomBytes[i] & 0xff); + } + + long leastSigBits = 0; + for (int i = 8; i < 16; i++) { + leastSigBits = (leastSigBits << 8) | (randomBytes[i] & 0xff); + } + + return new UUID(mostSigBits, leastSigBits); + } + +} \ No newline at end of file diff --git a/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy b/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy new file mode 100644 index 0000000000..3f010929db --- /dev/null +++ b/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy @@ -0,0 +1,19 @@ +package graphql.util + +import spock.lang.Specification + +class AlternativeJdkIdGeneratorTest extends Specification { + def "can generate uuids"() { + when: + def uuid1 = AlternativeJdkIdGenerator.uuid() + def uuid2 = AlternativeJdkIdGenerator.uuid() + def uuid3 = AlternativeJdkIdGenerator.uuid() + + then: + // should this fail - the universe has ended and has retracted back into the singularity + uuid1.toString() != uuid2.toString() + uuid1.toString() != uuid3.toString() + uuid2.toString() != uuid3.toString() + + } +} From 863eeaca2ca588ad82aaab1c5ec9157eff30715a Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 23 Feb 2024 09:54:59 +1100 Subject: [PATCH 239/393] Added an id generator that's more performant - added license --- APACHE-LICENSE-2.0.txt | 202 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 APACHE-LICENSE-2.0.txt diff --git a/APACHE-LICENSE-2.0.txt b/APACHE-LICENSE-2.0.txt new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/APACHE-LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file From 650ca9a98604f79a48835f13d25b9ba0ec3aef35 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 23 Feb 2024 13:23:27 +1100 Subject: [PATCH 240/393] Added Async benchmark for future changes --- src/test/java/benchmark/AsyncBenchmark.java | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/test/java/benchmark/AsyncBenchmark.java diff --git a/src/test/java/benchmark/AsyncBenchmark.java b/src/test/java/benchmark/AsyncBenchmark.java new file mode 100644 index 0000000000..84d0ee523a --- /dev/null +++ b/src/test/java/benchmark/AsyncBenchmark.java @@ -0,0 +1,78 @@ +package benchmark; + +import graphql.execution.Async; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 2) +@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +public class AsyncBenchmark { + + @Param({"0", "1", "10"}) + public int num; + + List> futures; + + @Setup(Level.Trial) + public void setUp() throws ExecutionException, InterruptedException { + futures = new ArrayList<>(); + for (int i = 0; i < num; i++) { + futures.add(mkFuture(i)); + } + + } + + private CompletableFuture mkFuture(int i) { + // half will take some time + if (i % 2 == 0) { + return CompletableFuture.supplyAsync(() -> sleep(i)); + } else { + return CompletableFuture.completedFuture(i); + } + } + + private Object sleep(int i) { + try { + Thread.sleep(i * 1000L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return i; + } + + @Benchmark + public List benchmarkAsync() { + Async.CombinedBuilder builder = Async.ofExpectedSize(futures.size()); + futures.forEach(builder::add); + return builder.await().join(); + } + + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include("benchmark.AsyncBenchmark") + .forks(5) + .build(); + + new Runner(opt).run(); + } + +} From ac371d171488d0d8a7660e20ea1362334ff7e612 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 23 Feb 2024 13:34:39 +1100 Subject: [PATCH 241/393] Changed test and move licenses --- .../APACHE-LICENSE-2.0.txt | 0 .../util/AlternativeJdkIdGeneratorTest.groovy | 12 +++++------- 2 files changed, 5 insertions(+), 7 deletions(-) rename APACHE-LICENSE-2.0.txt => additionallicenses/APACHE-LICENSE-2.0.txt (100%) diff --git a/APACHE-LICENSE-2.0.txt b/additionallicenses/APACHE-LICENSE-2.0.txt similarity index 100% rename from APACHE-LICENSE-2.0.txt rename to additionallicenses/APACHE-LICENSE-2.0.txt diff --git a/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy b/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy index 3f010929db..d897baa6e2 100644 --- a/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy +++ b/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy @@ -5,15 +5,13 @@ import spock.lang.Specification class AlternativeJdkIdGeneratorTest extends Specification { def "can generate uuids"() { when: - def uuid1 = AlternativeJdkIdGenerator.uuid() - def uuid2 = AlternativeJdkIdGenerator.uuid() - def uuid3 = AlternativeJdkIdGenerator.uuid() + def set = new HashSet() + for (int i = 0; i < 1000; i++) { + set.add(AlternativeJdkIdGenerator.uuid().toString()); + } then: // should this fail - the universe has ended and has retracted back into the singularity - uuid1.toString() != uuid2.toString() - uuid1.toString() != uuid3.toString() - uuid2.toString() != uuid3.toString() - + set.size() == 1000 } } From d1e0c6abd952cedcd3798ecf810e83881d19fdd6 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 23 Feb 2024 17:12:21 +1000 Subject: [PATCH 242/393] cleaning up the jhm benchmarks --- src/test/java/benchmark/AddError.java | 5 + .../java/benchmark/AstPrinterBenchmark.java | 6 +- src/test/java/benchmark/AsyncBenchmark.java | 3 +- src/test/java/benchmark/BenchMark.java | 14 +- .../ChainedInstrumentationBenchmark.java | 20 +- .../benchmark/DFSelectionSetBenchmark.java | 6 +- .../java/benchmark/GetterAccessBenchmark.java | 7 +- src/test/java/benchmark/IntMapBenchmark.java | 50 +++-- .../benchmark/IntrospectionBenchmark.java | 6 +- src/test/java/benchmark/ListBenchmark.java | 10 +- src/test/java/benchmark/NQBenchmark1.java | 19 +- src/test/java/benchmark/NQBenchmark2.java | 11 +- .../java/benchmark/NQExtraLargeBenchmark.java | 19 +- .../OverlappingFieldValidationBenchmark.java | 8 +- .../benchmark/PropertyFetcherBenchMark.java | 6 +- src/test/java/benchmark/SchemaBenchMark.java | 10 +- .../benchmark/SchemaTransformerBenchmark.java | 16 +- src/test/java/benchmark/TwitterBenchmark.java | 190 +++++++++--------- ...initionParserVersusSerializeBenchMark.java | 16 +- .../java/benchmark/ValidatorBenchmark.java | 27 ++- 20 files changed, 206 insertions(+), 243 deletions(-) diff --git a/src/test/java/benchmark/AddError.java b/src/test/java/benchmark/AddError.java index 950b756ab3..398f16d571 100644 --- a/src/test/java/benchmark/AddError.java +++ b/src/test/java/benchmark/AddError.java @@ -7,6 +7,7 @@ import graphql.schema.idl.errors.SchemaMissingError; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Scope; @@ -16,6 +17,9 @@ import java.util.Collections; @State(Scope.Benchmark) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class AddError { private final ExecutionContext context = new ExecutionContextBuilder() @@ -35,4 +39,5 @@ public ExecutionContext benchMarkAddError() { ); return context; } + } diff --git a/src/test/java/benchmark/AstPrinterBenchmark.java b/src/test/java/benchmark/AstPrinterBenchmark.java index 4f4a63d443..a764f3bc66 100644 --- a/src/test/java/benchmark/AstPrinterBenchmark.java +++ b/src/test/java/benchmark/AstPrinterBenchmark.java @@ -5,6 +5,7 @@ import graphql.parser.Parser; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; @@ -21,8 +22,9 @@ *

* Install it and then just hit "Run" on a certain benchmark method */ -@Warmup(iterations = 2, time = 5, batchSize = 3) -@Measurement(iterations = 3, time = 10, batchSize = 4) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3, time = 10) +@Fork(3) public class AstPrinterBenchmark { /** * Note: this query is a redacted version of a real query diff --git a/src/test/java/benchmark/AsyncBenchmark.java b/src/test/java/benchmark/AsyncBenchmark.java index 84d0ee523a..4a3482b855 100644 --- a/src/test/java/benchmark/AsyncBenchmark.java +++ b/src/test/java/benchmark/AsyncBenchmark.java @@ -3,6 +3,7 @@ import graphql.execution.Async; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; @@ -25,6 +26,7 @@ @BenchmarkMode(Mode.Throughput) @Warmup(iterations = 2) @Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +@Fork(2) public class AsyncBenchmark { @Param({"0", "1", "10"}) @@ -69,7 +71,6 @@ public List benchmarkAsync() { public static void main(String[] args) throws Exception { Options opt = new OptionsBuilder() .include("benchmark.AsyncBenchmark") - .forks(5) .build(); new Runner(opt).run(); diff --git a/src/test/java/benchmark/BenchMark.java b/src/test/java/benchmark/BenchMark.java index 29875d7841..ce49b6de8a 100644 --- a/src/test/java/benchmark/BenchMark.java +++ b/src/test/java/benchmark/BenchMark.java @@ -14,6 +14,7 @@ import graphql.schema.idl.TypeDefinitionRegistry; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; @@ -25,16 +26,9 @@ import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; -/** - * See this link for more samples - * on what you can do with JMH. - *

- * You MUST have the JMH plugin for IDEA in place for this to work : idea-jmh-plugin - *

- * Install it and then just hit "Run" on a certain benchmark method - */ -@Warmup(iterations = 2, time = 5, batchSize = 3) -@Measurement(iterations = 3, time = 10, batchSize = 4) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class BenchMark { private static final int NUMBER_OF_FRIENDS = 10 * 100; diff --git a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java index 129d266765..a0cf6ec33e 100644 --- a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java +++ b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java @@ -9,9 +9,17 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; -import org.jetbrains.annotations.NotNull; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; @@ -19,7 +27,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import static graphql.Scalars.GraphQLString; import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; @@ -27,8 +34,9 @@ @State(Scope.Benchmark) @BenchmarkMode(Mode.Throughput) -@Warmup(iterations = 2) -@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class ChainedInstrumentationBenchmark { @Param({"0", "1", "10"}) diff --git a/src/test/java/benchmark/DFSelectionSetBenchmark.java b/src/test/java/benchmark/DFSelectionSetBenchmark.java index 2687a444a0..654c484bef 100644 --- a/src/test/java/benchmark/DFSelectionSetBenchmark.java +++ b/src/test/java/benchmark/DFSelectionSetBenchmark.java @@ -29,9 +29,9 @@ import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) -@BenchmarkMode(Mode.Throughput) -@Warmup(iterations = 2) -@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class DFSelectionSetBenchmark { @State(Scope.Benchmark) diff --git a/src/test/java/benchmark/GetterAccessBenchmark.java b/src/test/java/benchmark/GetterAccessBenchmark.java index 7dd8c3d719..d98fbfcb47 100644 --- a/src/test/java/benchmark/GetterAccessBenchmark.java +++ b/src/test/java/benchmark/GetterAccessBenchmark.java @@ -2,6 +2,7 @@ import graphql.schema.fetching.LambdaFetchingSupport; import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; @@ -10,8 +11,9 @@ import java.lang.reflect.Method; import java.util.function.Function; -@Warmup(iterations = 2, time = 2, batchSize = 3) -@Measurement(iterations = 3, time = 2, batchSize = 4) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class GetterAccessBenchmark { public static class Pojo { @@ -69,3 +71,4 @@ public void measureReflectionAccess(Blackhole bh) { } } } + diff --git a/src/test/java/benchmark/IntMapBenchmark.java b/src/test/java/benchmark/IntMapBenchmark.java index a50b072908..2dd74732c0 100644 --- a/src/test/java/benchmark/IntMapBenchmark.java +++ b/src/test/java/benchmark/IntMapBenchmark.java @@ -2,9 +2,8 @@ import graphql.execution.instrumentation.dataloader.LevelMap; import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; @@ -12,34 +11,33 @@ import java.util.LinkedHashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) -@BenchmarkMode(Mode.Throughput) -@Warmup(iterations = 2) -@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class IntMapBenchmark { - @Benchmark - public void benchmarkLinkedHashMap(Blackhole blackhole) { - Map result = new LinkedHashMap<>(); - for (int i = 0; i < 30; i++) { - int level = i % 10; - int count = i * 2; - result.put(level, result.getOrDefault(level, 0) + count); - blackhole.consume(result.get(level)); - } - } + @Benchmark + public void benchmarkLinkedHashMap(Blackhole blackhole) { + Map result = new LinkedHashMap<>(); + for (int i = 0; i < 30; i++) { + int level = i % 10; + int count = i * 2; + result.put(level, result.getOrDefault(level, 0) + count); + blackhole.consume(result.get(level)); + } + } - @Benchmark - public void benchmarkIntMap(Blackhole blackhole) { - LevelMap result = new LevelMap(16); - for (int i = 0; i < 30; i++) { - int level = i % 10; - int count = i * 2; - result.increment(level, count); - blackhole.consume(result.get(level)); - } - } + @Benchmark + public void benchmarkIntMap(Blackhole blackhole) { + LevelMap result = new LevelMap(16); + for (int i = 0; i < 30; i++) { + int level = i % 10; + int count = i * 2; + result.increment(level, count); + blackhole.consume(result.get(level)); + } + } } diff --git a/src/test/java/benchmark/IntrospectionBenchmark.java b/src/test/java/benchmark/IntrospectionBenchmark.java index 6745d07d62..f6daf16d5e 100644 --- a/src/test/java/benchmark/IntrospectionBenchmark.java +++ b/src/test/java/benchmark/IntrospectionBenchmark.java @@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Scope; @@ -27,6 +28,9 @@ import java.util.Map; @State(Scope.Benchmark) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class IntrospectionBenchmark { private final GraphQL graphQL; @@ -116,8 +120,6 @@ public static > Map sortByValue(Map startingList = buildStartingList(); diff --git a/src/test/java/benchmark/NQBenchmark1.java b/src/test/java/benchmark/NQBenchmark1.java index d47d7b7a4c..09137f1bab 100644 --- a/src/test/java/benchmark/NQBenchmark1.java +++ b/src/test/java/benchmark/NQBenchmark1.java @@ -16,16 +16,15 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) -@BenchmarkMode(Mode.Throughput) -@Warmup(iterations = 2) -@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class NQBenchmark1 { @State(Scope.Benchmark) @@ -49,24 +48,16 @@ public void setup() { } @Benchmark - @Warmup(iterations = 2) - @Measurement(iterations = 3, time = 10) - @Threads(1) - @Fork(3) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) - public void benchMarkAvgTime(MyState myState, Blackhole blackhole ) { + public void benchMarkAvgTime(MyState myState, Blackhole blackhole) { runImpl(myState, blackhole); } @Benchmark - @Warmup(iterations = 2) - @Measurement(iterations = 3, time = 10) - @Threads(1) - @Fork(3) @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) - public void benchMarkThroughput(MyState myState, Blackhole blackhole ) { + public void benchMarkThroughput(MyState myState, Blackhole blackhole) { runImpl(myState, blackhole); } diff --git a/src/test/java/benchmark/NQBenchmark2.java b/src/test/java/benchmark/NQBenchmark2.java index 68402b4ec7..946f904d1e 100644 --- a/src/test/java/benchmark/NQBenchmark2.java +++ b/src/test/java/benchmark/NQBenchmark2.java @@ -23,7 +23,6 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import java.util.ArrayList; @@ -31,9 +30,9 @@ import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) -@BenchmarkMode(Mode.Throughput) -@Warmup(iterations = 2) -@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class NQBenchmark2 { @State(Scope.Benchmark) @@ -58,10 +57,6 @@ public void setup() { } @Benchmark - @Warmup(iterations = 2) - @Measurement(iterations = 5, time = 10) - @Threads(1) - @Fork(3) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) public ExecutableNormalizedOperation benchMarkAvgTime(MyState myState) { diff --git a/src/test/java/benchmark/NQExtraLargeBenchmark.java b/src/test/java/benchmark/NQExtraLargeBenchmark.java index fb92dbda9d..cc1453187a 100644 --- a/src/test/java/benchmark/NQExtraLargeBenchmark.java +++ b/src/test/java/benchmark/NQExtraLargeBenchmark.java @@ -16,16 +16,15 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) -@BenchmarkMode(Mode.Throughput) -@Warmup(iterations = 2) -@Measurement(iterations = 2, timeUnit = TimeUnit.NANOSECONDS) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class NQExtraLargeBenchmark { @State(Scope.Benchmark) @@ -49,24 +48,16 @@ public void setup() { } @Benchmark - @Warmup(iterations = 2) - @Measurement(iterations = 3, time = 10) - @Threads(1) - @Fork(3) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) - public void benchMarkAvgTime(MyState myState, Blackhole blackhole ) { + public void benchMarkAvgTime(MyState myState, Blackhole blackhole) { runImpl(myState, blackhole); } @Benchmark - @Warmup(iterations = 2) - @Measurement(iterations = 3, time = 10) - @Threads(1) - @Fork(3) @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) - public void benchMarkThroughput(MyState myState, Blackhole blackhole ) { + public void benchMarkThroughput(MyState myState, Blackhole blackhole) { runImpl(myState, blackhole); } diff --git a/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java b/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java index d8cf18bb5f..7340f9342b 100644 --- a/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java +++ b/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java @@ -22,7 +22,6 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; @@ -34,12 +33,9 @@ import static graphql.Assert.assertTrue; @State(Scope.Benchmark) -@BenchmarkMode(Mode.AverageTime) -@Threads(1) @Warmup(iterations = 2, time = 5) -@Measurement(iterations = 3, time = 10) +@Measurement(iterations = 3) @Fork(3) -@OutputTimeUnit(TimeUnit.MILLISECONDS) public class OverlappingFieldValidationBenchmark { @State(Scope.Benchmark) @@ -67,12 +63,12 @@ public void setup() { } @Benchmark + @BenchmarkMode(Mode.AverageTime) public void overlappingFieldValidationAbgTime(MyState myState, Blackhole blackhole) { blackhole.consume(validateQuery(myState.schema, myState.document)); } @Benchmark - @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public void overlappingFieldValidationThroughput(MyState myState, Blackhole blackhole) { blackhole.consume(validateQuery(myState.schema, myState.document)); diff --git a/src/test/java/benchmark/PropertyFetcherBenchMark.java b/src/test/java/benchmark/PropertyFetcherBenchMark.java index 036b4994b4..0de15a94cf 100644 --- a/src/test/java/benchmark/PropertyFetcherBenchMark.java +++ b/src/test/java/benchmark/PropertyFetcherBenchMark.java @@ -5,6 +5,7 @@ import graphql.schema.PropertyDataFetcher; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; @@ -23,8 +24,9 @@ *

* Install it and then just hit "Run" on a certain benchmark method */ -@Warmup(iterations = 2, time = 5, batchSize = 3) -@Measurement(iterations = 3, time = 10, batchSize = 4) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class PropertyFetcherBenchMark { @Benchmark diff --git a/src/test/java/benchmark/SchemaBenchMark.java b/src/test/java/benchmark/SchemaBenchMark.java index 23d35427e8..ba1520bd1e 100644 --- a/src/test/java/benchmark/SchemaBenchMark.java +++ b/src/test/java/benchmark/SchemaBenchMark.java @@ -1,6 +1,5 @@ package benchmark; -import com.google.common.io.Files; import graphql.schema.GraphQLSchema; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.SchemaGenerator; @@ -8,15 +7,13 @@ import graphql.schema.idl.TypeDefinitionRegistry; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.io.File; -import java.net.URL; -import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; /** @@ -29,8 +26,9 @@ *

* Install it and then just hit "Run" on a certain benchmark method */ -@Warmup(iterations = 2, time = 5, batchSize = 3) -@Measurement(iterations = 3, time = 10, batchSize = 4) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class SchemaBenchMark { static String largeSDL = BenchmarkUtils.loadResource("large-schema-3.graphqls"); diff --git a/src/test/java/benchmark/SchemaTransformerBenchmark.java b/src/test/java/benchmark/SchemaTransformerBenchmark.java index 96057aa534..669bda3f5e 100644 --- a/src/test/java/benchmark/SchemaTransformerBenchmark.java +++ b/src/test/java/benchmark/SchemaTransformerBenchmark.java @@ -1,7 +1,5 @@ package benchmark; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -22,20 +20,14 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; -import java.io.IOException; -import java.net.URL; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static com.google.common.io.Resources.getResource; - @State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) -@Threads(1) @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3, time = 10) @Fork(3) @@ -55,7 +47,7 @@ public static class MyState { @Override public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext context) { // add directive - GraphQLFieldDefinition changedNode = node.transform( builder -> { + GraphQLFieldDefinition changedNode = node.transform(builder -> { builder.withDirective(infoDirective); }); return changeNode(context, changedNode); @@ -64,7 +56,7 @@ public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, @Override public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext context) { // add directive info - GraphQLObjectType changedNode = node.transform( builder -> { + GraphQLObjectType changedNode = node.transform(builder -> { builder.withDirective(infoDirective); }); return changeNode(context, changedNode); @@ -78,7 +70,7 @@ public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, .filter(d -> !d.getName().equals(infoDirective.getName())) .collect(Collectors.toList()); // remove directive info - GraphQLFieldDefinition changedNode = node.transform( builder -> { + GraphQLFieldDefinition changedNode = node.transform(builder -> { builder.replaceDirectives(filteredDirectives); }); return changeNode(context, changedNode); @@ -90,7 +82,7 @@ public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, Traverser .filter(d -> !d.getName().equals(infoDirective.getName())) .collect(Collectors.toList()); // remove directive info - GraphQLObjectType changedNode = node.transform( builder -> { + GraphQLObjectType changedNode = node.transform(builder -> { builder.replaceDirectives(filteredDirectives); }); return changeNode(context, changedNode); diff --git a/src/test/java/benchmark/TwitterBenchmark.java b/src/test/java/benchmark/TwitterBenchmark.java index 4164f8a40b..c32753b7b5 100644 --- a/src/test/java/benchmark/TwitterBenchmark.java +++ b/src/test/java/benchmark/TwitterBenchmark.java @@ -19,7 +19,6 @@ import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; @@ -30,108 +29,107 @@ import static graphql.Scalars.GraphQLString; -@Warmup(iterations = 8, time = 10) -@Measurement(iterations = 25, time = 10) -@Fork(1) -@Threads(1) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class TwitterBenchmark { - private static final int BREADTH = 150; - private static final int DEPTH = 150; - - static String query = mkQuery(); - static Object queryId = "QUERY_ID"; - static GraphQL graphQL = buildGraphQL(); - - @Benchmark - @BenchmarkMode(Mode.Throughput) - @OutputTimeUnit(TimeUnit.SECONDS) - public void execute(Blackhole bh) { - bh.consume(execute()); - } - - private static ExecutionResult execute() { - return graphQL.execute(query); - } - - public static String mkQuery() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - for (int d=1; d <= DEPTH; d++) { - for (int b=1; b <= BREADTH; b++) { - sb.append("leaf_"); - sb.append(b); - sb.append(" "); - } - if (d < DEPTH) { - sb.append("branch { "); - } + private static final int BREADTH = 150; + private static final int DEPTH = 150; + + static String query = mkQuery(); + static Object queryId = "QUERY_ID"; + static GraphQL graphQL = buildGraphQL(); + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void execute(Blackhole bh) { + bh.consume(execute()); } - for (int d=1; d <= DEPTH; d++) { - sb.append("}"); + + private static ExecutionResult execute() { + return graphQL.execute(query); } - return sb.toString(); - } - - private static GraphQL buildGraphQL() { - ParserOptions.setDefaultOperationParserOptions(ParserOptions.newParserOptions().maxTokens(100_000).build()); - - List leafFields = new ArrayList<>(BREADTH); - for (int i = 1; i <= BREADTH; i++) { - leafFields.add( - GraphQLFieldDefinition.newFieldDefinition() - .name("leaf_" + i) - .type(GraphQLString) - .build() - ); + + public static String mkQuery() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (int d = 1; d <= DEPTH; d++) { + for (int b = 1; b <= BREADTH; b++) { + sb.append("leaf_"); + sb.append(b); + sb.append(" "); + } + if (d < DEPTH) { + sb.append("branch { "); + } + } + for (int d = 1; d <= DEPTH; d++) { + sb.append("}"); + } + return sb.toString(); } - GraphQLObjectType branchType = GraphQLObjectType.newObject() - .name("Branch") - .fields(leafFields) - .field(GraphQLFieldDefinition.newFieldDefinition() - .name("branch") - .type(GraphQLTypeReference.typeRef("Branch"))) - .build(); - - - DataFetcher simpleFetcher = env -> env.getField().getName(); - GraphQLCodeRegistry codeReg = GraphQLCodeRegistry.newCodeRegistry() - .defaultDataFetcher( - environment -> simpleFetcher - ) - .build(); - - GraphQLSchema graphQLSchema = GraphQLSchema.newSchema() - .query(branchType) - .codeRegistry(codeReg) - .build(); - - return GraphQL - .newGraphQL(graphQLSchema) - .preparsedDocumentProvider( - new PersistedQuery( - InMemoryPersistedQueryCache - .newInMemoryPersistedQueryCache() - .addQuery(queryId, query) - .build() - ) - ) - .build(); - } - - static class PersistedQuery extends PersistedQuerySupport { - public PersistedQuery(PersistedQueryCache persistedQueryCache) { - super(persistedQueryCache); + private static GraphQL buildGraphQL() { + ParserOptions.setDefaultOperationParserOptions(ParserOptions.newParserOptions().maxTokens(100_000).build()); + + List leafFields = new ArrayList<>(BREADTH); + for (int i = 1; i <= BREADTH; i++) { + leafFields.add( + GraphQLFieldDefinition.newFieldDefinition() + .name("leaf_" + i) + .type(GraphQLString) + .build() + ); + } + + GraphQLObjectType branchType = GraphQLObjectType.newObject() + .name("Branch") + .fields(leafFields) + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("branch") + .type(GraphQLTypeReference.typeRef("Branch"))) + .build(); + + + DataFetcher simpleFetcher = env -> env.getField().getName(); + GraphQLCodeRegistry codeReg = GraphQLCodeRegistry.newCodeRegistry() + .defaultDataFetcher( + environment -> simpleFetcher + ) + .build(); + + GraphQLSchema graphQLSchema = GraphQLSchema.newSchema() + .query(branchType) + .codeRegistry(codeReg) + .build(); + + return GraphQL + .newGraphQL(graphQLSchema) + .preparsedDocumentProvider( + new PersistedQuery( + InMemoryPersistedQueryCache + .newInMemoryPersistedQueryCache() + .addQuery(queryId, query) + .build() + ) + ) + .build(); } - @Override - protected Optional getPersistedQueryId(ExecutionInput executionInput) { - return Optional.of(queryId); + static class PersistedQuery extends PersistedQuerySupport { + public PersistedQuery(PersistedQueryCache persistedQueryCache) { + super(persistedQueryCache); + } + + @Override + protected Optional getPersistedQueryId(ExecutionInput executionInput) { + return Optional.of(queryId); + } } - } - public static void main(String[] args) { - ExecutionResult result = execute(); - System.out.println(result); - } + public static void main(String[] args) { + ExecutionResult result = execute(); + System.out.println(result); + } } diff --git a/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java b/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java index 931054bb72..8bec89914b 100644 --- a/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java +++ b/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java @@ -4,6 +4,7 @@ import graphql.schema.idl.TypeDefinitionRegistry; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; @@ -18,18 +19,9 @@ import static benchmark.BenchmarkUtils.asRTE; -/** - * This benchmarks {@link graphql.schema.idl.TypeDefinitionRegistry} parsing and serialisation - *

- * See https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/ for more samples - * on what you can do with JMH - *

- * You MUST have the JMH plugin for IDEA in place for this to work : https://github.com/artyushov/idea-jmh-plugin - *

- * Install it and then just hit "Run" on a certain benchmark method - */ -@Warmup(iterations = 2, time = 5, batchSize = 3) -@Measurement(iterations = 3, time = 10, batchSize = 4) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) public class TypeDefinitionParserVersusSerializeBenchMark { static SchemaParser schemaParser = new SchemaParser(); diff --git a/src/test/java/benchmark/ValidatorBenchmark.java b/src/test/java/benchmark/ValidatorBenchmark.java index 9db2384f29..71dc0aa33c 100644 --- a/src/test/java/benchmark/ValidatorBenchmark.java +++ b/src/test/java/benchmark/ValidatorBenchmark.java @@ -1,8 +1,12 @@ package benchmark; -import java.util.Locale; -import java.util.concurrent.TimeUnit; - +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.language.Document; +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaGenerator; +import graphql.validation.Validator; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -12,26 +16,19 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; -import graphql.ExecutionResult; -import graphql.GraphQL; -import graphql.language.Document; -import graphql.parser.Parser; -import graphql.schema.GraphQLSchema; -import graphql.schema.idl.SchemaGenerator; -import graphql.validation.Validator; +import java.util.Locale; +import java.util.concurrent.TimeUnit; import static graphql.Assert.assertTrue; @State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) -@Threads(1) -@Warmup(iterations = 5, time = 5) -@Measurement(iterations = 10, time = 10) -@Fork(1) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class ValidatorBenchmark { From d778868655d875843a33796f2dd96e6ffb5c3982 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:22:11 +1100 Subject: [PATCH 243/393] Add extensionDefinitions to schema builder --- .../java/graphql/schema/GraphQLSchema.java | 1 + .../graphql/schema/GraphQLSchemaTest.groovy | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index e365ef774b..313403c767 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -641,6 +641,7 @@ public static Builder newSchema(GraphQLSchema existingSchema) { .query(existingSchema.getQueryType()) .mutation(existingSchema.getMutationType()) .subscription(existingSchema.getSubscriptionType()) + .extensionDefinitions(existingSchema.getExtensionDefinitions()) .introspectionSchemaType(existingSchema.getIntrospectionSchemaType()) .codeRegistry(existingSchema.getCodeRegistry()) .clearAdditionalTypes() diff --git a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy index de2a51bf04..cd8b1c990f 100644 --- a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy @@ -5,6 +5,8 @@ import graphql.Directives import graphql.ExecutionInput import graphql.GraphQL import graphql.TestUtil +import graphql.language.Directive +import graphql.language.SchemaExtensionDefinition import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.TypeRuntimeWiring import graphql.util.TraversalControl @@ -129,6 +131,22 @@ class GraphQLSchemaTest extends Specification { )) } + def "schema builder copies extension definitions"() { + setup: + def schemaBuilder = basicSchemaBuilder() + def newDirective = Directive.newDirective().name("hello").build() + def extension = SchemaExtensionDefinition.newSchemaExtensionDefinition().directive(newDirective).build() + def oldSchema = schemaBuilder.extensionDefinitions([extension]).build() + + when: + def newSchema = GraphQLSchema.newSchema(oldSchema).build() + + then: + newSchema.extensionDefinitions.size() == 1 + ((Directive) newSchema.extensionDefinitions.first().getDirectives().first()).name == "hello" + + } + def "clear directives works as expected"() { setup: def schemaBuilder = basicSchemaBuilder() @@ -233,10 +251,8 @@ class GraphQLSchemaTest extends Specification { } union UnionType = Cat | Dog - ''' - when: def schema = TestUtil.schema(sdl) From 4854c233a7622ca884ac3231ebd0aea75e2c3fbf Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 23 Feb 2024 17:23:59 +1000 Subject: [PATCH 244/393] clean up --- src/test/java/benchmark/AddError.java | 43 -------- .../java/benchmark/AstPrinterBenchmark.java | 8 -- .../ChainedInstrumentationBenchmark.java | 2 +- ...chMark.java => CreateSchemaBenchmark.java} | 16 +-- .../benchmark/DFSelectionSetBenchmark.java | 9 -- .../{NQBenchmark1.java => ENFBenchmark1.java} | 2 +- src/test/java/benchmark/ENFBenchmark2.java | 59 ++++++++++ ...hmark.java => ENFExtraLargeBenchmark.java} | 2 +- .../java/benchmark/GetterAccessBenchmark.java | 4 +- src/test/java/benchmark/ListBenchmark.java | 63 ----------- src/test/java/benchmark/NQBenchmark2.java | 101 ------------------ .../benchmark/PropertyFetcherBenchMark.java | 14 +-- ...nchMark.java => SimpleQueryBenchmark.java} | 2 +- ...nitionParserVersusSerializeBenchmark.java} | 2 +- 14 files changed, 71 insertions(+), 256 deletions(-) delete mode 100644 src/test/java/benchmark/AddError.java rename src/test/java/benchmark/{SchemaBenchMark.java => CreateSchemaBenchmark.java} (75%) rename src/test/java/benchmark/{NQBenchmark1.java => ENFBenchmark1.java} (98%) create mode 100644 src/test/java/benchmark/ENFBenchmark2.java rename src/test/java/benchmark/{NQExtraLargeBenchmark.java => ENFExtraLargeBenchmark.java} (98%) delete mode 100644 src/test/java/benchmark/ListBenchmark.java delete mode 100644 src/test/java/benchmark/NQBenchmark2.java rename src/test/java/benchmark/{BenchMark.java => SimpleQueryBenchmark.java} (99%) rename src/test/java/benchmark/{TypeDefinitionParserVersusSerializeBenchMark.java => TypeDefinitionParserVersusSerializeBenchmark.java} (97%) diff --git a/src/test/java/benchmark/AddError.java b/src/test/java/benchmark/AddError.java deleted file mode 100644 index 398f16d571..0000000000 --- a/src/test/java/benchmark/AddError.java +++ /dev/null @@ -1,43 +0,0 @@ -package benchmark; - -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionContextBuilder; -import graphql.execution.ExecutionId; -import graphql.execution.ResultPath; -import graphql.schema.idl.errors.SchemaMissingError; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; - -import java.util.Collections; - -@State(Scope.Benchmark) -@Warmup(iterations = 2, time = 5) -@Measurement(iterations = 3) -@Fork(3) -public class AddError { - - private final ExecutionContext context = new ExecutionContextBuilder() - .executionId(ExecutionId.generate()) - .build(); - - private volatile int x = 0; - - @Benchmark - @BenchmarkMode(Mode.SingleShotTime) - @Warmup(iterations = 1, batchSize = 50000) - @Measurement(iterations = 1, batchSize = 5000) - public ExecutionContext benchMarkAddError() { - context.addError( - new SchemaMissingError(), - ResultPath.fromList(Collections.singletonList(x++)) - ); - return context; - } - -} diff --git a/src/test/java/benchmark/AstPrinterBenchmark.java b/src/test/java/benchmark/AstPrinterBenchmark.java index a764f3bc66..fd7f264523 100644 --- a/src/test/java/benchmark/AstPrinterBenchmark.java +++ b/src/test/java/benchmark/AstPrinterBenchmark.java @@ -14,14 +14,6 @@ import java.util.concurrent.TimeUnit; -/** - * See https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/ for more samples - * on what you can do with JMH - *

- * You MUST have the JMH plugin for IDEA in place for this to work : https://github.com/artyushov/idea-jmh-plugin - *

- * Install it and then just hit "Run" on a certain benchmark method - */ @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3, time = 10) @Fork(3) diff --git a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java index a0cf6ec33e..64e335e618 100644 --- a/src/test/java/benchmark/ChainedInstrumentationBenchmark.java +++ b/src/test/java/benchmark/ChainedInstrumentationBenchmark.java @@ -33,7 +33,6 @@ import static graphql.schema.GraphQLObjectType.newObject; @State(Scope.Benchmark) -@BenchmarkMode(Mode.Throughput) @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3) @Fork(3) @@ -69,6 +68,7 @@ public void setUp() throws ExecutionException, InterruptedException { } @Benchmark + @BenchmarkMode(Mode.Throughput) public GraphQLSchema benchmarkInstrumentSchema() { return chainedInstrumentation.instrumentSchema(schema, parameters, instrumentationState); } diff --git a/src/test/java/benchmark/SchemaBenchMark.java b/src/test/java/benchmark/CreateSchemaBenchmark.java similarity index 75% rename from src/test/java/benchmark/SchemaBenchMark.java rename to src/test/java/benchmark/CreateSchemaBenchmark.java index ba1520bd1e..0b5e67b41c 100644 --- a/src/test/java/benchmark/SchemaBenchMark.java +++ b/src/test/java/benchmark/CreateSchemaBenchmark.java @@ -16,34 +16,24 @@ import java.util.concurrent.TimeUnit; -/** - * This benchmarks schema creation - *

- * See https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/ for more samples - * on what you can do with JMH - *

- * You MUST have the JMH plugin for IDEA in place for this to work : https://github.com/artyushov/idea-jmh-plugin - *

- * Install it and then just hit "Run" on a certain benchmark method - */ @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3) @Fork(3) -public class SchemaBenchMark { +public class CreateSchemaBenchmark { static String largeSDL = BenchmarkUtils.loadResource("large-schema-3.graphqls"); @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MINUTES) - public void benchMarkLargeSchemaCreate(Blackhole blackhole) { + public void benchmarkLargeSchemaCreate(Blackhole blackhole) { blackhole.consume(createSchema(largeSDL)); } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) - public void benchMarkLargeSchemaCreateAvgTime(Blackhole blackhole) { + public void benchmarkLargeSchemaCreateAvgTime(Blackhole blackhole) { blackhole.consume(createSchema(largeSDL)); } diff --git a/src/test/java/benchmark/DFSelectionSetBenchmark.java b/src/test/java/benchmark/DFSelectionSetBenchmark.java index 654c484bef..01081cf51c 100644 --- a/src/test/java/benchmark/DFSelectionSetBenchmark.java +++ b/src/test/java/benchmark/DFSelectionSetBenchmark.java @@ -21,7 +21,6 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; @@ -65,10 +64,6 @@ public void setup() { } @Benchmark - @Warmup(iterations = 2) - @Measurement(iterations = 5, time = 10) - @Threads(1) - @Fork(3) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) public void benchMarkAvgTime(MyState myState, Blackhole blackhole) { @@ -77,10 +72,6 @@ public void benchMarkAvgTime(MyState myState, Blackhole blackhole) { } @Benchmark - @Warmup(iterations = 2) - @Measurement(iterations = 5, time = 10) - @Threads(1) - @Fork(3) @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) public void benchMarkThroughput(MyState myState, Blackhole blackhole) { diff --git a/src/test/java/benchmark/NQBenchmark1.java b/src/test/java/benchmark/ENFBenchmark1.java similarity index 98% rename from src/test/java/benchmark/NQBenchmark1.java rename to src/test/java/benchmark/ENFBenchmark1.java index 09137f1bab..6c10de1b19 100644 --- a/src/test/java/benchmark/NQBenchmark1.java +++ b/src/test/java/benchmark/ENFBenchmark1.java @@ -25,7 +25,7 @@ @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3) @Fork(3) -public class NQBenchmark1 { +public class ENFBenchmark1 { @State(Scope.Benchmark) public static class MyState { diff --git a/src/test/java/benchmark/ENFBenchmark2.java b/src/test/java/benchmark/ENFBenchmark2.java new file mode 100644 index 0000000000..a2a0ad3648 --- /dev/null +++ b/src/test/java/benchmark/ENFBenchmark2.java @@ -0,0 +1,59 @@ +package benchmark; + +import graphql.execution.CoercedVariables; +import graphql.language.Document; +import graphql.normalized.ExecutableNormalizedOperation; +import graphql.normalized.ExecutableNormalizedOperationFactory; +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaGenerator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(3) +public class ENFBenchmark2 { + + @State(Scope.Benchmark) + public static class MyState { + + GraphQLSchema schema; + Document document; + + @Setup + public void setup() { + try { + String schemaString = BenchmarkUtils.loadResource("large-schema-2.graphqls"); + schema = SchemaGenerator.createdMockedSchema(schemaString); + + String query = BenchmarkUtils.loadResource("large-schema-2-query.graphql"); + document = Parser.parse(query); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public ExecutableNormalizedOperation benchMarkAvgTime(MyState myState) { + ExecutableNormalizedOperation executableNormalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(myState.schema, myState.document, null, CoercedVariables.emptyVariables()); +// System.out.println("fields size:" + normalizedQuery.getFieldToNormalizedField().size()); + return executableNormalizedOperation; + } + +} diff --git a/src/test/java/benchmark/NQExtraLargeBenchmark.java b/src/test/java/benchmark/ENFExtraLargeBenchmark.java similarity index 98% rename from src/test/java/benchmark/NQExtraLargeBenchmark.java rename to src/test/java/benchmark/ENFExtraLargeBenchmark.java index cc1453187a..19a410a44d 100644 --- a/src/test/java/benchmark/NQExtraLargeBenchmark.java +++ b/src/test/java/benchmark/ENFExtraLargeBenchmark.java @@ -25,7 +25,7 @@ @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3) @Fork(3) -public class NQExtraLargeBenchmark { +public class ENFExtraLargeBenchmark { @State(Scope.Benchmark) public static class MyState { diff --git a/src/test/java/benchmark/GetterAccessBenchmark.java b/src/test/java/benchmark/GetterAccessBenchmark.java index d98fbfcb47..d7ff9b752c 100644 --- a/src/test/java/benchmark/GetterAccessBenchmark.java +++ b/src/test/java/benchmark/GetterAccessBenchmark.java @@ -11,8 +11,8 @@ import java.lang.reflect.Method; import java.util.function.Function; -@Warmup(iterations = 2, time = 5) -@Measurement(iterations = 3) +@Warmup(iterations = 2, time = 5, batchSize = 500) +@Measurement(iterations = 3, batchSize = 500) @Fork(3) public class GetterAccessBenchmark { diff --git a/src/test/java/benchmark/ListBenchmark.java b/src/test/java/benchmark/ListBenchmark.java deleted file mode 100644 index aaa00b91b9..0000000000 --- a/src/test/java/benchmark/ListBenchmark.java +++ /dev/null @@ -1,63 +0,0 @@ -package benchmark; - -import com.google.common.collect.ImmutableList; -import graphql.collect.ImmutableKit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -@State(Scope.Benchmark) -@Warmup(iterations = 2, time = 5) -@Measurement(iterations = 3) -@Fork(3) -public class ListBenchmark { - - static final List startingList = buildStartingList(); - - private static List buildStartingList() { - List list = new ArrayList<>(); - for (int i = 0; i < 10000; i++) { - list.add("String" + i); - } - return list; - } - - private final Function mapper = s -> new StringBuilder(s).reverse().toString(); - - @Benchmark - public void benchmarkListStream(Blackhole blackhole) { - List output = startingList.stream().map(mapper).collect(Collectors.toList()); - blackhole.consume(output); - } - - @Benchmark - public void benchmarkImmutableListBuilder(Blackhole blackhole) { - List output = ImmutableKit.map(startingList, mapper); - blackhole.consume(output); - } - - @Benchmark - public void benchmarkArrayList(Blackhole blackhole) { - List output = new ArrayList<>(startingList.size()); - for (String s : startingList) { - output.add(mapper.apply(s)); - } - blackhole.consume(output); - } - - @Benchmark - public void benchmarkImmutableCollectorBuilder(Blackhole blackhole) { - List output = startingList.stream().map(mapper).collect(ImmutableList.toImmutableList()); - blackhole.consume(output); - } - -} diff --git a/src/test/java/benchmark/NQBenchmark2.java b/src/test/java/benchmark/NQBenchmark2.java deleted file mode 100644 index 946f904d1e..0000000000 --- a/src/test/java/benchmark/NQBenchmark2.java +++ /dev/null @@ -1,101 +0,0 @@ -package benchmark; - -import com.google.common.collect.ImmutableListMultimap; -import graphql.execution.CoercedVariables; -import graphql.language.Document; -import graphql.language.Field; -import graphql.normalized.ExecutableNormalizedField; -import graphql.normalized.ExecutableNormalizedOperation; -import graphql.normalized.ExecutableNormalizedOperationFactory; -import graphql.parser.Parser; -import graphql.schema.GraphQLSchema; -import graphql.schema.idl.SchemaGenerator; -import graphql.util.TraversalControl; -import graphql.util.Traverser; -import graphql.util.TraverserContext; -import graphql.util.TraverserVisitorStub; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -@State(Scope.Benchmark) -@Warmup(iterations = 2, time = 5) -@Measurement(iterations = 3) -@Fork(3) -public class NQBenchmark2 { - - @State(Scope.Benchmark) - public static class MyState { - - GraphQLSchema schema; - Document document; - - @Setup - public void setup() { - try { - String schemaString = BenchmarkUtils.loadResource("large-schema-2.graphqls"); - schema = SchemaGenerator.createdMockedSchema(schemaString); - - String query = BenchmarkUtils.loadResource("large-schema-2-query.graphql"); - document = Parser.parse(query); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - } - - @Benchmark - @BenchmarkMode(Mode.AverageTime) - @OutputTimeUnit(TimeUnit.MILLISECONDS) - public ExecutableNormalizedOperation benchMarkAvgTime(MyState myState) { - ExecutableNormalizedOperation executableNormalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(myState.schema, myState.document, null, CoercedVariables.emptyVariables()); -// System.out.println("fields size:" + normalizedQuery.getFieldToNormalizedField().size()); - return executableNormalizedOperation; - } - - public static void main(String[] args) { - MyState myState = new MyState(); - myState.setup(); - ExecutableNormalizedOperation executableNormalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(myState.schema, myState.document, null, CoercedVariables.emptyVariables()); -// System.out.println(printTree(normalizedQuery)); - ImmutableListMultimap fieldToNormalizedField = executableNormalizedOperation.getFieldToNormalizedField(); - System.out.println(fieldToNormalizedField.size()); -// for (Field field : fieldToNormalizedField.keySet()) { -// System.out.println("field" + field); -// System.out.println("nf count:" + fieldToNormalizedField.get(field).size()); -// if (field.getName().equals("field49")) { -// ImmutableList normalizedFields = fieldToNormalizedField.get(field); -// for (NormalizedField nf : normalizedFields) { -// System.out.println(nf); -// } -// } -// } -// System.out.println("fields size:" + normalizedQuery.getFieldToNormalizedField().size()); - } - - static List printTree(ExecutableNormalizedOperation queryExecutionTree) { - List result = new ArrayList<>(); - Traverser traverser = Traverser.depthFirst(ExecutableNormalizedField::getChildren); - traverser.traverse(queryExecutionTree.getTopLevelFields(), new TraverserVisitorStub() { - @Override - public TraversalControl enter(TraverserContext context) { - ExecutableNormalizedField queryExecutionField = context.thisNode(); - result.add(queryExecutionField.printDetails()); - return TraversalControl.CONTINUE; - } - }); - return result; - } -} diff --git a/src/test/java/benchmark/PropertyFetcherBenchMark.java b/src/test/java/benchmark/PropertyFetcherBenchMark.java index 0de15a94cf..00146f8caf 100644 --- a/src/test/java/benchmark/PropertyFetcherBenchMark.java +++ b/src/test/java/benchmark/PropertyFetcherBenchMark.java @@ -14,18 +14,8 @@ import java.util.concurrent.TimeUnit; -/** - * This benchmarks a simple property fetch to help improve the key class PropertyDataFetcher - *

- * See https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples/ for more samples - * on what you can do with JMH - *

- * You MUST have the JMH plugin for IDEA in place for this to work : https://github.com/artyushov/idea-jmh-plugin - *

- * Install it and then just hit "Run" on a certain benchmark method - */ -@Warmup(iterations = 2, time = 5) -@Measurement(iterations = 3) +@Warmup(iterations = 2, time = 5, batchSize = 50) +@Measurement(iterations = 3, batchSize = 50) @Fork(3) public class PropertyFetcherBenchMark { diff --git a/src/test/java/benchmark/BenchMark.java b/src/test/java/benchmark/SimpleQueryBenchmark.java similarity index 99% rename from src/test/java/benchmark/BenchMark.java rename to src/test/java/benchmark/SimpleQueryBenchmark.java index ce49b6de8a..0cce5c0fb5 100644 --- a/src/test/java/benchmark/BenchMark.java +++ b/src/test/java/benchmark/SimpleQueryBenchmark.java @@ -29,7 +29,7 @@ @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3) @Fork(3) -public class BenchMark { +public class SimpleQueryBenchmark { private static final int NUMBER_OF_FRIENDS = 10 * 100; private static final GraphQL GRAPHQL = buildGraphQL(); diff --git a/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java b/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchmark.java similarity index 97% rename from src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java rename to src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchmark.java index 8bec89914b..995700d07f 100644 --- a/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java +++ b/src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchmark.java @@ -22,7 +22,7 @@ @Warmup(iterations = 2, time = 5) @Measurement(iterations = 3) @Fork(3) -public class TypeDefinitionParserVersusSerializeBenchMark { +public class TypeDefinitionParserVersusSerializeBenchmark { static SchemaParser schemaParser = new SchemaParser(); static String SDL = BenchmarkUtils.loadResource("large-schema-2.graphqls"); From 19a50f7081a4c99de37d3bb3a809b02399554919 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 23 Feb 2024 19:57:24 +1000 Subject: [PATCH 245/393] fix up loading resources from inside jar --- src/test/java/benchmark/BenchmarkUtils.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/test/java/benchmark/BenchmarkUtils.java b/src/test/java/benchmark/BenchmarkUtils.java index fd7897e125..cf92a1396d 100644 --- a/src/test/java/benchmark/BenchmarkUtils.java +++ b/src/test/java/benchmark/BenchmarkUtils.java @@ -1,9 +1,6 @@ package benchmark; -import com.google.common.io.Files; -import graphql.Assert; - -import java.io.File; +import java.io.InputStream; import java.net.URL; import java.nio.charset.Charset; import java.util.concurrent.Callable; @@ -17,7 +14,11 @@ static String loadResource(String name) { if (resource == null) { throw new IllegalArgumentException("missing resource: " + name); } - return String.join("\n", Files.readLines(new File(resource.toURI()), Charset.defaultCharset())); + byte[] bytes; + try (InputStream inputStream = resource.openStream()) { + bytes = inputStream.readAllBytes(); + } + return new String(bytes, Charset.defaultCharset()); }); } From 78f6243b631aa6047a35524d3a5908f3683171e4 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 24 Feb 2024 14:01:16 +1100 Subject: [PATCH 246/393] fixed async benchmark - it was non sensical before --- src/test/java/benchmark/AsyncBenchmark.java | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/test/java/benchmark/AsyncBenchmark.java b/src/test/java/benchmark/AsyncBenchmark.java index 4a3482b855..cc52db226e 100644 --- a/src/test/java/benchmark/AsyncBenchmark.java +++ b/src/test/java/benchmark/AsyncBenchmark.java @@ -29,37 +29,24 @@ @Fork(2) public class AsyncBenchmark { - @Param({"0", "1", "10"}) - public int num; + @Param({"1", "5", "20"}) + public int numberOfFieldCFs; List> futures; @Setup(Level.Trial) public void setUp() throws ExecutionException, InterruptedException { futures = new ArrayList<>(); - for (int i = 0; i < num; i++) { + for (int i = 0; i < numberOfFieldCFs; i++) { futures.add(mkFuture(i)); } } private CompletableFuture mkFuture(int i) { - // half will take some time - if (i % 2 == 0) { - return CompletableFuture.supplyAsync(() -> sleep(i)); - } else { - return CompletableFuture.completedFuture(i); - } + return CompletableFuture.completedFuture(i); } - private Object sleep(int i) { - try { - Thread.sleep(i * 1000L); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - return i; - } @Benchmark public List benchmarkAsync() { From b66841730d8362dd54df12388ebce5b4990de8ce Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sat, 24 Feb 2024 14:08:46 +1100 Subject: [PATCH 247/393] Tidy up --- .../java/graphql/schema/GraphQLSchema.java | 18 +++++++++--------- .../graphql/schema/GraphQLSchemaTest.groovy | 7 ++++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 313403c767..e7026ad6c3 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -71,7 +71,7 @@ public class GraphQLSchema { private final ImmutableMap> interfaceNameToObjectTypeNames; /* - * This constructs partial GraphQL schema object which has has the schema (query / mutation / subscription) trees + * This constructs partial GraphQL schema object which has the schema (query / mutation / subscription) trees * in it but it does not have the collected types, code registry nor the type references replaced * * But it can be traversed to discover all that and filled out later via another constructor. @@ -104,7 +104,7 @@ private GraphQLSchema(Builder builder) { } /* - * This constructs a full fledged graphql schema object that has not yet had its type references replaced + * This constructs a fully fledged graphql schema object that has not yet had its type references replaced * but it's otherwise complete */ @Internal @@ -255,7 +255,7 @@ public List getTypes(Collection typeNames) { /** * Gets the named type from the schema or null if it's not present. - * + *

* Warning - you are inviting class cast errors if the types are not what you expect. * * @param typeName the name of the type to retrieve @@ -286,7 +286,7 @@ public boolean containsType(String typeName) { * * @return a graphql object type or null if there is one * - * @throws graphql.GraphQLException if the type is NOT a object type + * @throws graphql.GraphQLException if the type is NOT an object type */ public GraphQLObjectType getObjectType(String typeName) { GraphQLType graphQLType = typeMap.get(typeName); @@ -433,14 +433,14 @@ public List getDirectives() { } /** - * @return a map of non repeatable directives by directive name + * @return a map of non-repeatable directives by directive name */ public Map getDirectivesByName() { return directiveDefinitionsHolder.getDirectivesByName(); } /** - * Returns a named directive that (for legacy reasons) will be only in the set of non repeatable directives + * Returns a named directive that (for legacy reasons) will be only in the set of non-repeatable directives * * @param directiveName the name of the directive to retrieve * @@ -535,7 +535,7 @@ public List getSchemaDirectives(String directiveName) { * directives for all schema elements, whereas this is just for the schema * element itself * - * @return a map of directives + * @return a list of directives */ public List getSchemaAppliedDirectives() { return schemaAppliedDirectivesHolder.getAppliedDirectives(); @@ -874,8 +874,8 @@ public GraphQLSchema build(Set additionalTypes) { /** * Builds the schema * - * @param additionalTypes - please don't use this any more - * @param additionalDirectives - please don't use this any more + * @param additionalTypes - please don't use this anymore + * @param additionalDirectives - please don't use this anymore * * @return the built schema * diff --git a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy index cd8b1c990f..bf18e083c2 100644 --- a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy @@ -134,7 +134,7 @@ class GraphQLSchemaTest extends Specification { def "schema builder copies extension definitions"() { setup: def schemaBuilder = basicSchemaBuilder() - def newDirective = Directive.newDirective().name("hello").build() + def newDirective = Directive.newDirective().name("pizza").build() def extension = SchemaExtensionDefinition.newSchemaExtensionDefinition().directive(newDirective).build() def oldSchema = schemaBuilder.extensionDefinitions([extension]).build() @@ -142,9 +142,10 @@ class GraphQLSchemaTest extends Specification { def newSchema = GraphQLSchema.newSchema(oldSchema).build() then: + oldSchema.extensionDefinitions.size() == 1 newSchema.extensionDefinitions.size() == 1 - ((Directive) newSchema.extensionDefinitions.first().getDirectives().first()).name == "hello" - + ((Directive) oldSchema.extensionDefinitions.first().getDirectives().first()).name == "pizza" + ((Directive) newSchema.extensionDefinitions.first().getDirectives().first()).name == "pizza" } def "clear directives works as expected"() { From 6d5d373119efe2b571cfaf8719a7fc8f75bca1bb Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 24 Feb 2024 16:09:08 +1100 Subject: [PATCH 248/393] Tweaked to be slightly faster and how it allocates results --- src/main/java/graphql/execution/Async.java | 67 +++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index b186b6c8c3..6d2f9d6bd0 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -22,6 +22,13 @@ @SuppressWarnings("FutureReturnValueIgnored") public class Async { + /** + * A builder of materialized objects or {@link CompletableFuture}s than can present a promise to the list of them + *

+ * This builder has a strict contract on size whereby if the expectedSize is 5 then there MUST be five elements presented to it. + * + * @param for two + */ public interface CombinedBuilder { /** @@ -158,26 +165,26 @@ private static class Many implements CombinedBuilder { private final Object[] array; private int ix; - private boolean containsCFs; + private int cfCount; @SuppressWarnings("unchecked") private Many(int size) { this.array = new Object[size]; this.ix = 0; - containsCFs = false; + cfCount = 0; } @Override public void add(CompletableFuture completableFuture) { array[ix++] = completableFuture; - containsCFs = true; + cfCount++; } @Override public void addObject(T objectT) { array[ix++] = objectT; if (objectT instanceof CompletableFuture) { - containsCFs = true; + cfCount++; } } @@ -187,10 +194,10 @@ public CompletableFuture> await() { commonSizeAssert(); CompletableFuture> overallResult = new CompletableFuture<>(); - if (!containsCFs) { + if (cfCount == 0) { overallResult.complete(materialisedList(array)); } else { - CompletableFuture[] cfsArr = copyOnlyCFsToArray(); + CompletableFuture[] cfsArr = copyOnlyCFsToArray(); CompletableFuture.allOf(cfsArr) .whenComplete((ignored, exception) -> { if (exception != null) { @@ -198,13 +205,21 @@ public CompletableFuture> await() { return; } List results = new ArrayList<>(array.length); - for (Object object : array) { - if (object instanceof CompletableFuture) { - CompletableFuture cf = (CompletableFuture) object; - // join is safe since they are all completed earlier via CompletableFuture.allOf() + if (cfsArr.length == array.length) { + // they are all CFs + for (CompletableFuture cf : cfsArr) { results.add(cf.join()); - } else { - results.add((T) object); + } + } else { + // it's a mixed bag of CFs and materialized objects + for (Object object : array) { + if (object instanceof CompletableFuture) { + CompletableFuture cf = (CompletableFuture) object; + // join is safe since they are all completed earlier via CompletableFuture.allOf() + results.add(cf.join()); + } else { + results.add((T) object); + } } } overallResult.complete(results); @@ -213,9 +228,28 @@ public CompletableFuture> await() { return overallResult; } + @SuppressWarnings("unchecked") + @NotNull + private CompletableFuture[] copyOnlyCFsToArray() { + if (cfCount == array.length) { + // if it's all CFs - make a type safe copy via C code + return Arrays.copyOf(array, array.length, CompletableFuture[].class); + } else { + int i = 0; + CompletableFuture[] dest = new CompletableFuture[cfCount]; + for (Object o : array) { + if (o instanceof CompletableFuture) { + dest[i] = (CompletableFuture) o; + i++; + } + } + return dest; + } + } + @Override public Object awaitPolymorphic() { - if (!containsCFs) { + if (cfCount == 0) { commonSizeAssert(); return materialisedList(array); } else { @@ -237,13 +271,6 @@ private void commonSizeAssert() { Assert.assertTrue(ix == array.length, () -> "expected size was " + array.length + " got " + ix); } - @NotNull - private CompletableFuture[] copyOnlyCFsToArray() { - return Arrays.stream(array) - .filter(obj -> obj instanceof CompletableFuture) - .toArray(CompletableFuture[]::new); - } - } @SuppressWarnings("unchecked") From 54144fa1c1b01e4761a4b6beafa747494308a9c2 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 24 Feb 2024 16:29:38 +1100 Subject: [PATCH 249/393] annotation not needed --- src/main/java/graphql/execution/Async.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index 6d2f9d6bd0..a9d8420984 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -167,7 +167,6 @@ private static class Many implements CombinedBuilder { private int ix; private int cfCount; - @SuppressWarnings("unchecked") private Many(int size) { this.array = new Object[size]; this.ix = 0; From 5b517ccf4765fa1dbd8aeb41d8cdc53cc0da1d4b Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Mon, 26 Feb 2024 12:45:13 +1000 Subject: [PATCH 250/393] Removing tests where validation already has set as invalid queries. --- ...NormalizedOperationFactoryDeferTest.groovy | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 332d1d685e..f861fcf222 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -647,34 +647,6 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "multiple fields and a multiple defers with same label are not allowed"() { - given: - - String query = ''' - query q { - dog { - ... @defer(label:"name-defer") { - name - } - - ... @defer(label:"name-defer") { - name - } - } - } - - ''' - - Map variables = [:] - - when: - executeQueryAndPrintTree(query, variables) - - then: - def exception = thrown(AssertException) - exception.message == "Internal error: should never happen: Duplicated @defer labels are not allowed: [name-defer]" - } - def "nested defers - no label"() { given: @@ -861,35 +833,6 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "'if' argument with different values on same field and same label"() { - given: - - String query = ''' - query q { - dog { - ... @defer(if: false, label: "name-defer") { - name - } - - ... @defer(if: true, label: "name-defer") { - name - } - } - } - - ''' - - Map variables = [:] - - when: - List printedTree = executeQueryAndPrintTree(query, variables) - - then: - printedTree == ['Query.dog', - 'Dog.name defer{[label=name-defer;types=[Dog]]}', - ] - } - private ExecutableNormalizedOperation createExecutableNormalizedOperations(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) From e7b732802ff9ea94d5e897ab582439e735096446 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 26 Feb 2024 15:35:59 +1100 Subject: [PATCH 251/393] andis suggestions on batch size --- src/test/java/benchmark/AsyncBenchmark.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/benchmark/AsyncBenchmark.java b/src/test/java/benchmark/AsyncBenchmark.java index cc52db226e..a2fa43addd 100644 --- a/src/test/java/benchmark/AsyncBenchmark.java +++ b/src/test/java/benchmark/AsyncBenchmark.java @@ -49,6 +49,8 @@ private CompletableFuture mkFuture(int i) { @Benchmark + @Warmup(iterations = 2, batchSize = 100) + @Measurement(iterations = 2, batchSize = 100) public List benchmarkAsync() { Async.CombinedBuilder builder = Async.ofExpectedSize(futures.size()); futures.forEach(builder::add); From a1ba0d013cedabbacfd848985cf9530fca5358b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 16:57:19 +0000 Subject: [PATCH 252/393] Bump google-github-actions/auth from 2.1.1 to 2.1.2 Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/google-github-actions/auth/releases) - [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/auth/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: google-github-actions/auth dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/invoke_test_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index ea710cb715..16b6e15c61 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -50,7 +50,7 @@ jobs: - id: 'auth' name: 'Authenticate to Google Cloud' - uses: google-github-actions/auth@v2.1.1 + uses: google-github-actions/auth@v2.1.2 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} From 47b8924c7b7b0b25c9f2abcff149df55740dc88a Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 27 Feb 2024 12:56:06 +1100 Subject: [PATCH 253/393] Tweaked id generator name --- src/main/java/graphql/execution/ExecutionId.java | 6 ++---- .../{AlternativeJdkIdGenerator.java => IdGenerator.java} | 9 ++++++--- ...eJdkIdGeneratorTest.groovy => IdGeneratorTest.groovy} | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) rename src/main/java/graphql/util/{AlternativeJdkIdGenerator.java => IdGenerator.java} (92%) rename src/test/groovy/graphql/util/{AlternativeJdkIdGeneratorTest.groovy => IdGeneratorTest.groovy} (72%) diff --git a/src/main/java/graphql/execution/ExecutionId.java b/src/main/java/graphql/execution/ExecutionId.java index f36b1bf750..933a5369c8 100644 --- a/src/main/java/graphql/execution/ExecutionId.java +++ b/src/main/java/graphql/execution/ExecutionId.java @@ -2,9 +2,7 @@ import graphql.Assert; import graphql.PublicApi; -import graphql.util.AlternativeJdkIdGenerator; - -import java.util.UUID; +import graphql.util.IdGenerator; /** * This opaque identifier is used to identify a unique query execution @@ -18,7 +16,7 @@ public class ExecutionId { * @return a query execution identifier */ public static ExecutionId generate() { - return new ExecutionId(AlternativeJdkIdGenerator.uuid().toString()); + return new ExecutionId(IdGenerator.uuid().toString()); } /** diff --git a/src/main/java/graphql/util/AlternativeJdkIdGenerator.java b/src/main/java/graphql/util/IdGenerator.java similarity index 92% rename from src/main/java/graphql/util/AlternativeJdkIdGenerator.java rename to src/main/java/graphql/util/IdGenerator.java index 719aad255d..f92784c4ac 100644 --- a/src/main/java/graphql/util/AlternativeJdkIdGenerator.java +++ b/src/main/java/graphql/util/IdGenerator.java @@ -21,6 +21,8 @@ */ package graphql.util; +import graphql.Internal; + import java.math.BigInteger; import java.security.SecureRandom; import java.util.Random; @@ -33,9 +35,10 @@ * @author Rossen Stoyanchev * @author Rob Winch */ -public class AlternativeJdkIdGenerator { +@Internal +public class IdGenerator { - private static AlternativeJdkIdGenerator idGenerator = new AlternativeJdkIdGenerator(); + private static final IdGenerator idGenerator = new IdGenerator(); public static UUID uuid() { return idGenerator.generateId(); @@ -44,7 +47,7 @@ public static UUID uuid() { private final Random random; - public AlternativeJdkIdGenerator() { + public IdGenerator() { SecureRandom secureRandom = new SecureRandom(); byte[] seed = new byte[8]; secureRandom.nextBytes(seed); diff --git a/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy b/src/test/groovy/graphql/util/IdGeneratorTest.groovy similarity index 72% rename from src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy rename to src/test/groovy/graphql/util/IdGeneratorTest.groovy index d897baa6e2..99a9344012 100644 --- a/src/test/groovy/graphql/util/AlternativeJdkIdGeneratorTest.groovy +++ b/src/test/groovy/graphql/util/IdGeneratorTest.groovy @@ -2,12 +2,12 @@ package graphql.util import spock.lang.Specification -class AlternativeJdkIdGeneratorTest extends Specification { +class IdGeneratorTest extends Specification { def "can generate uuids"() { when: def set = new HashSet() for (int i = 0; i < 1000; i++) { - set.add(AlternativeJdkIdGenerator.uuid().toString()); + set.add(IdGenerator.uuid().toString()); } then: From 2557467e4ec2bc62d81fc08eaf943c09a512ff46 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 27 Feb 2024 18:58:46 +1100 Subject: [PATCH 254/393] Added a more complex query benchmark --- .../java/benchmark/ComplexQueryBenchmark.java | 258 ++++++++++++++++++ .../resources/storesanddepartments.graphqls | 42 ++- 2 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 src/test/java/benchmark/ComplexQueryBenchmark.java diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java new file mode 100644 index 0000000000..64f4ce3ef5 --- /dev/null +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -0,0 +1,258 @@ +package benchmark; + +import com.google.common.collect.ImmutableList; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.profile.GCProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; + +/** + * This benchmark is an attempt to have a more complex query that involves async and sync work together + * along with multiple threads happening. + *

+ * It can also be run in a forever mode say if you want to connect a profiler to it say + */ +@State(Scope.Benchmark) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 2) +@Fork(1) +public class ComplexQueryBenchmark { + + @Param({"5", "10", "20"}) + int howManyThreads = 5; + @Param({"10", "50", "100"}) + int howManyQueries = 10; + @Param({"5", "20", "100"}) + int howManyItems = 5; + @Param({"1", "5", "10"}) + int howLongToSleep = 1; + + ExecutorService executorService; + GraphQL graphQL; + volatile boolean shutDown; + + @Setup(Level.Trial) + public void setUp() { + shutDown = false; + executorService = Executors.newFixedThreadPool(howManyThreads); + graphQL = buildGraphQL(); + } + + @TearDown(Level.Trial) + public void tearDown() { + shutDown = true; + executorService.shutdownNow(); + } + + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public Object benchMarkSimpleQueriesThroughput() { + return runManyQueriesToCompletion(); + } + + + public static void main(String[] args) throws Exception { + // just to make sure it's all valid before testing + runAtStartup(); + + Options opt = new OptionsBuilder() + .include("benchmark.ComplexQueryBenchmark") + .addProfiler(GCProfiler.class) + .build(); + + new Runner(opt).run(); + } + + @SuppressWarnings({"ConstantValue", "LoopConditionNotUpdatedInsideLoop"}) + private static void runAtStartup() { + // set this to true if you want to hook in profiler say to a forever running JVM + boolean forever = false; + + long then = System.currentTimeMillis(); + System.out.printf("Running initial code before starting the benchmark in forever=%b mode \n", forever); + ComplexQueryBenchmark complexQueryBenchmark = new ComplexQueryBenchmark(); + complexQueryBenchmark.setUp(); + do { + complexQueryBenchmark.runManyQueriesToCompletion(); + } while (forever); + complexQueryBenchmark.tearDown(); + + System.out.printf("This took %d millis\n", System.currentTimeMillis() - then); + + } + + @SuppressWarnings("UnnecessaryLocalVariable") + private Void runManyQueriesToCompletion() { + CompletableFuture[] cfs = new CompletableFuture[howManyQueries]; + for (int i = 0; i < howManyQueries; i++) { + cfs[i] = CompletableFuture.supplyAsync(() -> executeQuery(howManyItems, howLongToSleep), executorService); + } + Void result = CompletableFuture.allOf(cfs).join(); + return result; + } + + @SuppressWarnings("UnnecessaryLocalVariable") + public ExecutionResult executeQuery(int howMany, int howLong) { + String fields = "id name f1 f2 f3 f4 f5 f6 f7 f8 f9 f10"; + String query = "query q {" + + String.format("shops(howMany : %d) { %s departments( howMany : %d) { %s products(howMany : %d) { %s }}}\n" + , howMany, fields, howMany, fields, howMany, fields) + + String.format("expensiveShops(howMany : %d howLong : %d) { %s expensiveDepartments( howMany : %d howLong : %d) { %s expensiveProducts(howMany : %d howLong : %d) { %s }}}\n" + , howMany, howLong, fields, howMany, howLong, fields, howMany, howLong, fields) + + "}"; + ExecutionResult executionResult = graphQL.execute(query); + return executionResult; + } + + private GraphQL buildGraphQL() { + TypeDefinitionRegistry definitionRegistry = new SchemaParser().parse(BenchmarkUtils.loadResource("storesanddepartments.graphqls")); + + DataFetcher shopsDF = env -> mkHowManyThings(env.getArgument("howMany")); + DataFetcher expensiveShopsDF = env -> supplyAsync(() -> sleepAndReturnThings(env)); + DataFetcher departmentsDF = env -> mkHowManyThings(env.getArgument("howMany")); + DataFetcher expensiveDepartmentsDF = env -> supplyAsync(() -> sleepAndReturnThings(env)); + DataFetcher productsDF = env -> mkHowManyThings(env.getArgument("howMany")); + DataFetcher expensiveProductsDF = env -> supplyAsync(() -> sleepAndReturnThings(env)); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(newTypeWiring("Query") + .dataFetcher("shops", shopsDF) + .dataFetcher("expensiveShops", expensiveShopsDF)) + .type(newTypeWiring("Shop") + .dataFetcher("departments", departmentsDF) + .dataFetcher("expensiveDepartments", expensiveDepartmentsDF)) + .type(newTypeWiring("Department") + .dataFetcher("products", productsDF) + .dataFetcher("expensiveProducts", expensiveProductsDF)) + .build(); + + GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(definitionRegistry, runtimeWiring); + + return GraphQL.newGraphQL(graphQLSchema).build(); + } + + private CompletableFuture supplyAsync(Supplier codeToRun) { + if (!shutDown) { + return CompletableFuture.supplyAsync(codeToRun); + } else { + // if we have shutdown - get on with it, so we shut down quicker + return CompletableFuture.completedFuture(codeToRun.get()); + } + } + + private List sleepAndReturnThings(DataFetchingEnvironment env) { + // by sleeping, we hope to cause the objects to stay longer in GC land and hence have a longer lifecycle + // then a simple stack say or young gen gc. I don't know this will work, but I am trying it + // to represent work that takes some tie to complete + sleep(env.getArgument("howLong")); + return mkHowManyThings(env.getArgument("howMany")); + } + + private void sleep(Integer howLong) { + if (howLong > 0) { + try { + Thread.sleep(howLong); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private List mkHowManyThings(Integer howMany) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < howMany; i++) { + builder.add(new IdAndNamedThing(i)); + } + return builder.build(); + } + + @SuppressWarnings("unused") + static class IdAndNamedThing { + private final int i; + + public IdAndNamedThing(int i) { + this.i = i; + } + + public String getId() { + return "id" + i; + } + + public String getName() { + return "name" + i; + } + + public String getF1() { + return "f1" + i; + } + + public String getF2() { + return "f2" + i; + } + + public String getF3() { + return "f3" + i; + } + + public String getF4() { + return "f4" + i; + } + + public String getF5() { + return "f5" + i; + } + + public String getF6() { + return "f6" + i; + } + + public String getF7() { + return "f7" + i; + } + + public String getF8() { + return "f8" + i; + } + + public String getF9() { + return "f9" + i; + } + + public String getF10() { + return "f10" + i; + } + } +} diff --git a/src/test/resources/storesanddepartments.graphqls b/src/test/resources/storesanddepartments.graphqls index 8a8defd3e8..57c83e3ab7 100644 --- a/src/test/resources/storesanddepartments.graphqls +++ b/src/test/resources/storesanddepartments.graphqls @@ -4,25 +4,55 @@ schema { } type Query { - shops: [Shop] - expensiveShops: [Shop] + shops(howMany : Int = 5): [Shop] + expensiveShops(howMany : Int = 5, howLong : Int = 0): [Shop] } type Shop { id: ID! name: String! - departments: [Department] - expensiveDepartments: [Department] + f1 : String + f2 : String + f3 : String + f4 : String + f5 : String + f6 : String + f7 : String + f8 : String + f9 : String + f10 : String + departments(howMany : Int = 5): [Department] + expensiveDepartments(howMany : Int = 5, howLong : Int = 0): [Department] } type Department { id: ID! name: String! - products: [Product] - expensiveProducts: [Product] + f1 : String + f2 : String + f3 : String + f4 : String + f5 : String + f6 : String + f7 : String + f8 : String + f9 : String + f10 : String + products(howMany : Int = 5): [Product] + expensiveProducts(howMany : Int = 5, howLong : Int = 0): [Product] } type Product { id: ID! name: String! + f1 : String + f2 : String + f3 : String + f4 : String + f5 : String + f6 : String + f7 : String + f8 : String + f9 : String + f10 : String } From 1f55a8e87b2cdc8bd5b24c61eaef007b7a88b705 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 27 Feb 2024 22:08:11 +1100 Subject: [PATCH 255/393] To many dimensions --- .../java/benchmark/ComplexQueryBenchmark.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index 64f4ce3ef5..dc4265a8f9 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -49,30 +49,31 @@ @Fork(1) public class ComplexQueryBenchmark { - @Param({"5", "10", "20"}) - int howManyThreads = 5; - @Param({"10", "50", "100"}) - int howManyQueries = 10; @Param({"5", "20", "100"}) int howManyItems = 5; - @Param({"1", "5", "10"}) - int howLongToSleep = 1; + int howLongToSleep = 5; + int howManyQueries = 50; + int howManyQueryThreads = 10; + int howManyFetcherThreads = 10; - ExecutorService executorService; + ExecutorService queryExecutorService; + ExecutorService fetchersExecutorService; GraphQL graphQL; volatile boolean shutDown; @Setup(Level.Trial) public void setUp() { shutDown = false; - executorService = Executors.newFixedThreadPool(howManyThreads); + queryExecutorService = Executors.newFixedThreadPool(howManyQueryThreads); + fetchersExecutorService = Executors.newFixedThreadPool(howManyFetcherThreads); graphQL = buildGraphQL(); } @TearDown(Level.Trial) public void tearDown() { shutDown = true; - executorService.shutdownNow(); + queryExecutorService.shutdownNow(); + fetchersExecutorService.shutdownNow(); } @@ -106,6 +107,7 @@ private static void runAtStartup() { ComplexQueryBenchmark complexQueryBenchmark = new ComplexQueryBenchmark(); complexQueryBenchmark.setUp(); do { + System.out.print("Running queries....\n"); complexQueryBenchmark.runManyQueriesToCompletion(); } while (forever); complexQueryBenchmark.tearDown(); @@ -118,7 +120,7 @@ private static void runAtStartup() { private Void runManyQueriesToCompletion() { CompletableFuture[] cfs = new CompletableFuture[howManyQueries]; for (int i = 0; i < howManyQueries; i++) { - cfs[i] = CompletableFuture.supplyAsync(() -> executeQuery(howManyItems, howLongToSleep), executorService); + cfs[i] = CompletableFuture.supplyAsync(() -> executeQuery(howManyItems, howLongToSleep), queryExecutorService); } Void result = CompletableFuture.allOf(cfs).join(); return result; @@ -166,7 +168,7 @@ private GraphQL buildGraphQL() { private CompletableFuture supplyAsync(Supplier codeToRun) { if (!shutDown) { - return CompletableFuture.supplyAsync(codeToRun); + return CompletableFuture.supplyAsync(codeToRun, fetchersExecutorService); } else { // if we have shutdown - get on with it, so we shut down quicker return CompletableFuture.completedFuture(codeToRun.get()); From efc66364e511cf1df868f32fcb99b00ad367917b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 28 Feb 2024 13:23:52 +1100 Subject: [PATCH 256/393] Reduced howMany objects are created when multipled out by levels --- .../java/benchmark/ComplexQueryBenchmark.java | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index dc4265a8f9..0ff7859afe 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -1,6 +1,7 @@ package benchmark; import com.google.common.collect.ImmutableList; +import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.schema.DataFetcher; @@ -33,6 +34,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; @@ -108,6 +110,7 @@ private static void runAtStartup() { complexQueryBenchmark.setUp(); do { System.out.print("Running queries....\n"); + complexQueryBenchmark.howManyItems = 100; complexQueryBenchmark.runManyQueriesToCompletion(); } while (forever); complexQueryBenchmark.tearDown(); @@ -120,23 +123,21 @@ private static void runAtStartup() { private Void runManyQueriesToCompletion() { CompletableFuture[] cfs = new CompletableFuture[howManyQueries]; for (int i = 0; i < howManyQueries; i++) { - cfs[i] = CompletableFuture.supplyAsync(() -> executeQuery(howManyItems, howLongToSleep), queryExecutorService); + cfs[i] = CompletableFuture.supplyAsync(() -> executeQuery(howManyItems, howLongToSleep), queryExecutorService).thenCompose(cf -> cf); } Void result = CompletableFuture.allOf(cfs).join(); return result; } - @SuppressWarnings("UnnecessaryLocalVariable") - public ExecutionResult executeQuery(int howMany, int howLong) { + public CompletableFuture executeQuery(int howMany, int howLong) { String fields = "id name f1 f2 f3 f4 f5 f6 f7 f8 f9 f10"; String query = "query q {" + String.format("shops(howMany : %d) { %s departments( howMany : %d) { %s products(howMany : %d) { %s }}}\n" - , howMany, fields, howMany, fields, howMany, fields) + , howMany, fields, 10, fields, 5, fields) + String.format("expensiveShops(howMany : %d howLong : %d) { %s expensiveDepartments( howMany : %d howLong : %d) { %s expensiveProducts(howMany : %d howLong : %d) { %s }}}\n" - , howMany, howLong, fields, howMany, howLong, fields, howMany, howLong, fields) + , howMany, howLong, fields, 10, howLong, fields, 5, howLong, fields) + "}"; - ExecutionResult executionResult = graphQL.execute(query); - return executionResult; + return graphQL.executeAsync(ExecutionInput.newExecutionInput(query).build()); } private GraphQL buildGraphQL() { @@ -145,9 +146,9 @@ private GraphQL buildGraphQL() { DataFetcher shopsDF = env -> mkHowManyThings(env.getArgument("howMany")); DataFetcher expensiveShopsDF = env -> supplyAsync(() -> sleepAndReturnThings(env)); DataFetcher departmentsDF = env -> mkHowManyThings(env.getArgument("howMany")); - DataFetcher expensiveDepartmentsDF = env -> supplyAsync(() -> sleepAndReturnThings(env)); + DataFetcher expensiveDepartmentsDF = env -> supplyAsyncListItems(env, () -> sleepAndReturnThings(env)); DataFetcher productsDF = env -> mkHowManyThings(env.getArgument("howMany")); - DataFetcher expensiveProductsDF = env -> supplyAsync(() -> sleepAndReturnThings(env)); + DataFetcher expensiveProductsDF = env -> supplyAsyncListItems(env, () -> sleepAndReturnThings(env)); RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() .type(newTypeWiring("Query") @@ -166,8 +167,13 @@ private GraphQL buildGraphQL() { return GraphQL.newGraphQL(graphQLSchema).build(); } + private CompletableFuture supplyAsyncListItems(DataFetchingEnvironment environment, Supplier codeToRun) { + return supplyAsync(codeToRun); + } + private CompletableFuture supplyAsync(Supplier codeToRun) { if (!shutDown) { + //logEvery(100, "async fetcher"); return CompletableFuture.supplyAsync(codeToRun, fetchersExecutorService); } else { // if we have shutdown - get on with it, so we shut down quicker @@ -193,6 +199,14 @@ private void sleep(Integer howLong) { } } + AtomicInteger logCount = new AtomicInteger(); + private void logEvery(int every, String s) { + int count = logCount.getAndIncrement(); + if (count == 0 || count % every == 0) { + System.out.println("\t" + count + "\t" + s); + } + } + private List mkHowManyThings(Integer howMany) { ImmutableList.Builder builder = ImmutableList.builder(); for (int i = 0; i < howMany; i++) { From 5cb7f012a483f4255aa5904fef440f30665b2f90 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 28 Feb 2024 15:34:24 +1100 Subject: [PATCH 257/393] Put in a forever mode flag --- src/test/java/benchmark/ComplexQueryBenchmark.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index 0ff7859afe..a0160f1065 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -102,7 +102,7 @@ public static void main(String[] args) throws Exception { @SuppressWarnings({"ConstantValue", "LoopConditionNotUpdatedInsideLoop"}) private static void runAtStartup() { // set this to true if you want to hook in profiler say to a forever running JVM - boolean forever = false; + boolean forever = Boolean.getBoolean("forever"); long then = System.currentTimeMillis(); System.out.printf("Running initial code before starting the benchmark in forever=%b mode \n", forever); From 77283155d9fa280e4b6478ed9023835f43260e7b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 28 Feb 2024 18:41:23 +1100 Subject: [PATCH 258/393] removing parameters not needed on dispatch --- .../java/graphql/execution/DataLoaderDispatchStrategy.java | 5 ++--- src/main/java/graphql/execution/ExecutionStrategy.java | 2 +- .../dataloader/PerLevelDataLoaderDispatchStrategy.java | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java index 7cdc32d980..bb5bbb8d76 100644 --- a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java @@ -39,9 +39,8 @@ default void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyP } default void fieldFetched(ExecutionContext executionContext, - ExecutionStrategyParameters executionStrategyParameters, - DataFetcher dataFetcher, - CompletableFuture fetchedValue) { + ExecutionStrategyParameters executionStrategyParameters + ) { } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 0d79d326b8..4fe4415b2e 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -424,7 +424,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); - executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); + executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters); fetchCtx.onDispatched(fetchedValue); return fetchedValue .handle((result, exception) -> { diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index 9ac4e0d181..27bae57203 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -186,7 +186,7 @@ private int getCountForList(List fieldValueInfos) { @Override - public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters, DataFetcher dataFetcher, CompletableFuture fetchedValue) { + public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters) { int level = executionStrategyParameters.getPath().getLevel(); boolean dispatchNeeded = callStack.lock.callLocked(() -> { callStack.increaseFetchCount(level); From 31bf7abe78cd1f9793ebdd29b0ea959dab3a44cf Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 28 Feb 2024 19:19:05 +1100 Subject: [PATCH 259/393] ok not removing them - changing them --- .../java/graphql/execution/DataLoaderDispatchStrategy.java | 5 +++-- src/main/java/graphql/execution/ExecutionStrategy.java | 2 +- .../dataloader/PerLevelDataLoaderDispatchStrategy.java | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java index bb5bbb8d76..ee3c0bd97c 100644 --- a/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/DataLoaderDispatchStrategy.java @@ -39,8 +39,9 @@ default void executeObjectOnFieldValuesException(Throwable t, ExecutionStrategyP } default void fieldFetched(ExecutionContext executionContext, - ExecutionStrategyParameters executionStrategyParameters - ) { + ExecutionStrategyParameters executionStrategyParameters, + DataFetcher dataFetcher, + Object fetchedValue) { } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 4fe4415b2e..0d79d326b8 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -424,7 +424,7 @@ protected CompletableFuture fetchField(ExecutionContext executionC dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); - executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters); + executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); fetchCtx.onDispatched(fetchedValue); return fetchedValue .handle((result, exception) -> { diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java index 27bae57203..a407346954 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/PerLevelDataLoaderDispatchStrategy.java @@ -14,7 +14,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.CompletableFuture; @Internal public class PerLevelDataLoaderDispatchStrategy implements DataLoaderDispatchStrategy { @@ -186,7 +185,10 @@ private int getCountForList(List fieldValueInfos) { @Override - public void fieldFetched(ExecutionContext executionContext, ExecutionStrategyParameters executionStrategyParameters) { + public void fieldFetched(ExecutionContext executionContext, + ExecutionStrategyParameters executionStrategyParameters, + DataFetcher dataFetcher, + Object fetchedValue) { int level = executionStrategyParameters.getPath().getLevel(); boolean dispatchNeeded = callStack.lock.callLocked(() -> { callStack.increaseFetchCount(level); From 154f6f294c70d63eda385593983f92594a36ea3b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 10:29:25 +1100 Subject: [PATCH 260/393] extra method needed for DF value auto casting --- src/main/java/graphql/execution/Async.java | 16 ++++++++++++++++ .../groovy/graphql/execution/AsyncTest.groovy | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index a9d8420984..ed74f32ef8 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -365,6 +365,22 @@ public static CompletableFuture toCompletableFuture(T t) { } } + /** + * Turns a= CompletionStage into a CompletableFuture if it's not already, otherwise leaves it alone + * as a materialized object + * + * @param object - the object to check + * + * @return a CompletableFuture from a CompletionStage or the object itself + */ + public static Object toCompletableFutureOrMaterializedObject(Object object) { + if (object instanceof CompletionStage) { + return ((CompletionStage) object).toCompletableFuture(); + } else { + return object; + } + } + public static CompletableFuture tryCatch(Supplier> supplier) { try { return supplier.get(); diff --git a/src/test/groovy/graphql/execution/AsyncTest.groovy b/src/test/groovy/graphql/execution/AsyncTest.groovy index 34929b0bff..b9e6c2664b 100644 --- a/src/test/groovy/graphql/execution/AsyncTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncTest.groovy @@ -381,6 +381,21 @@ class AsyncTest extends Specification { joinOrMaterialized(awaited) == ["A"] } + def "toCompletableFutureOrMaterializedObject tested"() { + def x = "x" + def cf = completedFuture(x) + + when: + def object = Async.toCompletableFutureOrMaterializedObject(x) + then: + object == x + + when: + object = Async.toCompletableFutureOrMaterializedObject(cf) + then: + object == cf + } + Object joinOrMaterialized(Object awaited) { if (awaited instanceof CompletableFuture) { return ((CompletableFuture) awaited).join() From 703941ae9bd3e150a538952668f855768b7cf917 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 11:56:38 +1100 Subject: [PATCH 261/393] merged was bad - fixed up --- src/main/java/graphql/GraphQL.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 64509368e0..096a4f7e26 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -420,7 +420,7 @@ public CompletableFuture executeAsync(ExecutionInput executionI InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInputWithId, this.graphQLSchema); ExecutionInput instrumentedExecutionInput = instrumentation.instrumentExecutionInput(executionInputWithId, inputInstrumentationParameters, instrumentationState); - InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(instrumentedExecutionInput, this.graphQLSchema, instrumentationState); + InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(instrumentedExecutionInput, this.graphQLSchema); InstrumentationContext executionInstrumentation = nonNullCtx(instrumentation.beginExecution(instrumentationParameters, instrumentationState)); executionInstrumentation.onDispatched(); @@ -523,7 +523,7 @@ private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchem } private List validate(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema, instrumentationState), instrumentationState)); + InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema), instrumentationState)); validationCtx.onDispatched(); Predicate> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true); From 9f32ad5b058ce6cf20d7601bbbccd6d16846ad26 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 15:38:45 +1100 Subject: [PATCH 262/393] First working version of less CFs --- src/main/java/graphql/execution/Async.java | 6 +- .../graphql/execution/ExecutionStrategy.java | 139 +++++++++++------- .../graphql/execution/FieldValueInfo.java | 24 ++- .../SubscriptionExecutionStrategy.java | 2 +- .../BreadthFirstExecutionTestStrategy.java | 2 +- .../execution/BreadthFirstTestStrategy.java | 2 +- .../execution/FieldValueInfoTest.groovy | 2 +- 7 files changed, 111 insertions(+), 66 deletions(-) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index ed74f32ef8..dc61cd83ad 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -356,12 +356,12 @@ private static void eachSequentiallyPolymorphicImpl(Iterator iterator, * * @return a CompletableFuture */ - public static CompletableFuture toCompletableFuture(T t) { + @SuppressWarnings("unchecked") + public static CompletableFuture toCompletableFuture(Object t) { if (t instanceof CompletionStage) { - //noinspection unchecked return ((CompletionStage) t).toCompletableFuture(); } else { - return CompletableFuture.completedFuture(t); + return CompletableFuture.completedFuture((T) t); } } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 615699583c..e9df03988b 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -269,7 +269,7 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi fields, parameters, executionContext, - this::resolveFieldWithInfo + (ec, esp) -> Async.toCompletableFuture(resolveFieldWithInfo(ec, esp)) ) : DeferredExecutionSupport.NOOP; } @@ -296,8 +296,13 @@ Async.CombinedBuilder getAsyncFieldValueInfo( .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); if (!deferredExecutionSupport.isDeferredField(currentField)) { - CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); - futures.add(future); + Object fieldValueInfo = resolveFieldWithInfo(executionContext, newParameters); + if (fieldValueInfo instanceof CompletableFuture) { + //noinspection unchecked + futures.add((CompletableFuture) fieldValueInfo); + } else { + futures.addObject((FieldValueInfo) fieldValueInfo); + } } } return futures; @@ -319,8 +324,14 @@ Async.CombinedBuilder getAsyncFieldValueInfo( * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ - protected CompletableFuture resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - return resolveFieldWithInfo(executionContext, parameters).thenCompose(FieldValueInfo::getFieldValueFuture); + @SuppressWarnings("unchecked") + protected Object /* CompletableFuture | Object */ resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + Object fieldWithInfo = resolveFieldWithInfo(executionContext, parameters); + if (fieldWithInfo instanceof CompletableFuture) { + return ((CompletableFuture) fieldWithInfo).thenCompose(FieldValueInfo::getFieldValueFuture); + } else { + return ((FieldValueInfo) fieldWithInfo).getFieldValueObject(); + } } /** @@ -339,7 +350,8 @@ protected CompletableFuture resolveField(ExecutionContext executionConte * * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value */ - protected CompletableFuture resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + @SuppressWarnings("unchecked") + protected Object /* CompletableFuture | FieldValueInfo */ resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); @@ -348,15 +360,28 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() )); - CompletableFuture fetchFieldFuture = fetchField(executionContext, parameters); - CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> - completeField(executionContext, parameters, fetchedValue)); + Object fetchedValueObj = fetchField(executionContext, parameters); + if (fetchedValueObj instanceof CompletableFuture) { + CompletableFuture fetchFieldFuture = (CompletableFuture) fetchedValueObj; + CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> + completeField(executionContext, parameters, fetchedValue)); - CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); + CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); - fieldCtx.onDispatched(); - fieldValueFuture.whenComplete(fieldCtx::onCompleted); - return result; + fieldCtx.onDispatched(); + fieldValueFuture.whenComplete(fieldCtx::onCompleted); + return result; + } else { + try { + FetchedValue fetchedValue = (FetchedValue) fetchedValueObj; + FieldValueInfo fieldValueInfo = completeField(executionContext, parameters, fetchedValue); + fieldCtx.onDispatched(); + fieldCtx.onCompleted(fetchedValue.getFetchedValue(), null); + return fieldValueInfo; + } catch (Exception e) { + return Async.exceptionallyCompletedFuture(e); + } + } } /** @@ -369,11 +394,11 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * - * @return a promise to a fetched object + * @return a promise to a {@link FetchedValue} object or the {@link FetchedValue} itself * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ - protected CompletableFuture fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + protected Object /*CompletableFuture | FetchedValue>*/ fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { MergedField field = parameters.getField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField()); @@ -423,25 +448,32 @@ protected CompletableFuture fetchField(ExecutionContext executionC dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); - CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); - executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedValue); + Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); + executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject); fetchCtx.onDispatched(); - return fetchedValue - .handle((result, exception) -> { - fetchCtx.onCompleted(result, exception); - if (exception != null) { - return handleFetchingException(dataFetchingEnvironment.get(), parameters, exception); - } else { - // we can simply return the fetched value CF and avoid a allocation - return fetchedValue; - } - }) - .thenCompose(Function.identity()) - .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); + if (fetchedObject instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletableFuture fetchedValue = (CompletableFuture) fetchedObject; + return fetchedValue + .handle((result, exception) -> { + fetchCtx.onCompleted(result, exception); + if (exception != null) { + return handleFetchingException(dataFetchingEnvironment.get(), parameters, exception); + } else { + // we can simply return the fetched value CF and avoid a allocation + return fetchedValue; + } + }) + .thenCompose(Function.identity()) + .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); + } else { + fetchCtx.onCompleted(fetchedObject, null); + return unboxPossibleDataFetcherResult(executionContext, parameters, fetchedObject); + } } - private CompletableFuture invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { - CompletableFuture fetchedValue; + private Object invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { + Object fetchedValue; try { Object fetchedValueRaw; if (dataFetcher instanceof LightDataFetcher) { @@ -449,7 +481,7 @@ private CompletableFuture invokeDataFetcher(ExecutionContext executionCo } else { fetchedValueRaw = dataFetcher.get(dataFetchingEnvironment.get()); } - fetchedValue = Async.toCompletableFuture(fetchedValueRaw); + fetchedValue = Async.toCompletableFutureOrMaterializedObject(fetchedValueRaw); } catch (Exception e) { fetchedValue = Async.exceptionallyCompletedFuture(e); } @@ -600,7 +632,7 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); Object result = executionContext.getValueUnboxer().unbox(parameters.getSource()); GraphQLType fieldType = executionStepInfo.getUnwrappedNonNullType(); - CompletableFuture fieldValue; + Object fieldValue; if (result == null) { return getFieldValueInfoForNull(parameters); @@ -608,10 +640,10 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut return completeValueForList(executionContext, parameters, result); } else if (isScalar(fieldType)) { fieldValue = completeValueForScalar(executionContext, parameters, (GraphQLScalarType) fieldType, result); - return FieldValueInfo.newFieldValueInfo(SCALAR).fieldValue(fieldValue).build(); + return FieldValueInfo.newFieldValueInfo(SCALAR).fieldValueObject(fieldValue).build(); } else if (isEnum(fieldType)) { fieldValue = completeValueForEnum(executionContext, parameters, (GraphQLEnumType) fieldType, result); - return FieldValueInfo.newFieldValueInfo(ENUM).fieldValue(fieldValue).build(); + return FieldValueInfo.newFieldValueInfo(ENUM).fieldValueObject(fieldValue).build(); } // when we are here, we have a complex type: Interface, Union or Object @@ -628,7 +660,7 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut // complete field as null, validating it is nullable return getFieldValueInfoForNull(parameters); } - return FieldValueInfo.newFieldValueInfo(OBJECT).fieldValue(fieldValue).build(); + return FieldValueInfo.newFieldValueInfo(OBJECT).fieldValueObject(fieldValue).build(); } private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStrategyParameters parameters, UnresolvedTypeException e) { @@ -648,15 +680,16 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters parameters) { - CompletableFuture fieldValue = completeValueForNull(parameters); - return FieldValueInfo.newFieldValueInfo(NULL).fieldValue(fieldValue).build(); + Object fieldValue = completeValueForNull(parameters); + return FieldValueInfo.newFieldValueInfo(NULL).fieldValueObject(fieldValue).build(); } - protected CompletableFuture completeValueForNull(ExecutionStrategyParameters parameters) { - return Async.tryCatch(() -> { - Object nullValue = parameters.getNonNullFieldValidator().validate(parameters.getPath(), null); - return completedFuture(nullValue); - }); + protected Object /* CompletableFuture | Object */ completeValueForNull(ExecutionStrategyParameters parameters) { + try { + return parameters.getNonNullFieldValidator().validate(parameters.getPath(), null); + } catch (Exception e) { + return Async.exceptionallyCompletedFuture(e); + } } /** @@ -674,10 +707,10 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, try { resultIterable = parameters.getNonNullFieldValidator().validate(parameters.getPath(), resultIterable); } catch (NonNullableFieldWasNullException e) { - return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(exceptionallyCompletedFuture(e)).build(); + return FieldValueInfo.newFieldValueInfo(LIST).fieldValueObject(exceptionallyCompletedFuture(e)).build(); } if (resultIterable == null) { - return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(completedFuture(null)).build(); + return FieldValueInfo.newFieldValueInfo(LIST).fieldValueObject(completedFuture(null)).build(); } return completeValueForList(executionContext, parameters, resultIterable); } @@ -743,7 +776,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, }); return FieldValueInfo.newFieldValueInfo(LIST) - .fieldValue(overallResult) + .fieldValueObject(overallResult) .fieldValueInfos(fieldValueInfos) .build(); } @@ -779,9 +812,9 @@ protected void handleValueException(CompletableFuture overallResult, Thro * @param scalarType the type of the scalar * @param result the result to be coerced * - * @return a promise to an {@link ExecutionResult} + * @return a materialized scalar value or exceptionally completed {@link CompletableFuture} if there is a problem */ - protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { + protected Object /* CompletableFuture | Object */ completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { Object serialized; try { serialized = scalarType.getCoercing().serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); @@ -794,20 +827,20 @@ protected CompletableFuture completeValueForScalar(ExecutionContext exec } catch (NonNullableFieldWasNullException e) { return exceptionallyCompletedFuture(e); } - return completedFuture(serialized); + return serialized; } /** - * Called to turn an object into a enum value according to the {@link GraphQLEnumType} by asking that enum type to coerce the object into a valid value + * Called to turn an object into an enum value according to the {@link GraphQLEnumType} by asking that enum type to coerce the object into a valid value * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param enumType the type of the enum * @param result the result to be coerced * - * @return a promise to an {@link ExecutionResult} + * @return a materialized enum value or exceptionally completed {@link CompletableFuture} if there is a problem */ - protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { + protected Object /* CompletableFuture | Object */ completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { Object serialized; try { serialized = enumType.serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); @@ -819,7 +852,7 @@ protected CompletableFuture completeValueForEnum(ExecutionContext execut } catch (NonNullableFieldWasNullException e) { return exceptionallyCompletedFuture(e); } - return completedFuture(serialized); + return serialized; } /** diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index 571bbaed1a..6a5e988633 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -24,12 +24,14 @@ public enum CompleteValueType { private final CompleteValueType completeValueType; private final CompletableFuture fieldValue; + private final Object /* CompletableFuture | Object */ fieldValueObject; private final List fieldValueInfos; - private FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { + private FieldValueInfo(CompleteValueType completeValueType, Object fieldValueObject, List fieldValueInfos) { assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); this.completeValueType = completeValueType; - this.fieldValue = fieldValue; + this.fieldValueObject = fieldValueObject; + this.fieldValue = Async.toCompletableFuture(fieldValueObject); this.fieldValueInfos = fieldValueInfos; } @@ -44,6 +46,9 @@ public CompletableFuture getFieldValue() { public CompletableFuture getFieldValueFuture() { return fieldValue; } + public Object /* CompletableFuture | Object */ getFieldValueObject() { + return fieldValue; + } public List getFieldValueInfos() { return fieldValueInfos; @@ -65,7 +70,7 @@ public String toString() { @SuppressWarnings("unused") public static class Builder { private CompleteValueType completeValueType; - private CompletableFuture fieldValueFuture; + private Object fieldValueObject; private List listInfos = new ArrayList<>(); public Builder(CompleteValueType completeValueType) { @@ -77,8 +82,15 @@ public Builder completeValueType(CompleteValueType completeValueType) { return this; } - public Builder fieldValue(CompletableFuture executionResultFuture) { - this.fieldValueFuture = executionResultFuture; +// KILL for now in the PR - probably want to kill this anyway for reals +// +// public Builder fieldValue(CompletableFuture executionResultFuture) { +// this.fieldValueObject = executionResultFuture; +// return this; +// } + + public Builder fieldValueObject(Object fieldValueObject) { + this.fieldValueObject = fieldValueObject; return this; } @@ -89,7 +101,7 @@ public Builder fieldValueInfos(List listInfos) { } public FieldValueInfo build() { - return new FieldValueInfo(completeValueType, fieldValueFuture, listInfos); + return new FieldValueInfo(completeValueType, fieldValueObject, listInfos); } } } \ No newline at end of file diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index e6beb4bd04..7a7cd5c952 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -93,7 +93,7 @@ public CompletableFuture execute(ExecutionContext executionCont private CompletableFuture> createSourceEventStream(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { ExecutionStrategyParameters newParameters = firstFieldOfSubscriptionSelection(parameters); - CompletableFuture fieldFetched = fetchField(executionContext, newParameters); + CompletableFuture fieldFetched = Async.toCompletableFuture(fetchField(executionContext, newParameters)); return fieldFetched.thenApply(fetchedValue -> { Object publisher = fetchedValue.getFetchedValue(); if (publisher != null) { diff --git a/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java b/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java index 5d22041e1b..167e58a3b9 100644 --- a/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java +++ b/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java @@ -58,7 +58,7 @@ private FetchedValue fetchField(ExecutionContext executionContext, ExecutionStra ExecutionStrategyParameters newParameters = parameters .transform(builder -> builder.field(currentField).path(fieldPath)); - return fetchField(executionContext, newParameters).join(); + return Async.toCompletableFuture(fetchField(executionContext, newParameters)).join(); } private void completeValue(ExecutionContext executionContext, Map results, String fieldName, FetchedValue fetchedValue, ExecutionStrategyParameters newParameters) { diff --git a/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java b/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java index f934904c78..653e035df1 100644 --- a/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java +++ b/src/test/groovy/graphql/execution/BreadthFirstTestStrategy.java @@ -41,7 +41,7 @@ private Map fetchFields(ExecutionContext executionContext, for (String fieldName : fields.keySet()) { ExecutionStrategyParameters newParameters = newParameters(parameters, fields, fieldName); - CompletableFuture fetchFuture = fetchField(executionContext, newParameters); + CompletableFuture fetchFuture = Async.toCompletableFuture(fetchField(executionContext, newParameters)); fetchFutures.put(fieldName, fetchFuture); } diff --git a/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy b/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy index f34666fa22..450fd322d2 100644 --- a/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy +++ b/src/test/groovy/graphql/execution/FieldValueInfoTest.groovy @@ -14,7 +14,7 @@ class FieldValueInfoTest extends Specification{ fieldValueInfo.fieldValueInfos == [] as List and: "other fields to be null " - fieldValueInfo.fieldValueFuture == null + fieldValueInfo.fieldValueFuture.join() == null fieldValueInfo.completeValueType == null } From 274aaf1d84fd6e29e8751a3dc24363a26077353f Mon Sep 17 00:00:00 2001 From: Juliano Prado Date: Thu, 29 Feb 2024 17:04:39 +1000 Subject: [PATCH 263/393] Changing SubscriptionUniqueRootField in order to also check inline fragments and multiple fragments --- .../rules/SubscriptionUniqueRootField.java | 50 ++++--- .../SubscriptionUniqueRootFieldTest.groovy | 136 +++++++++++++++++- 2 files changed, 162 insertions(+), 24 deletions(-) diff --git a/src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java b/src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java index bd73db3dd5..0ded9ca632 100644 --- a/src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java +++ b/src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java @@ -1,11 +1,15 @@ package graphql.validation.rules; import graphql.Internal; -import graphql.language.Field; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; +import graphql.execution.CoercedVariables; +import graphql.execution.FieldCollector; +import graphql.execution.FieldCollectorParameters; +import graphql.execution.MergedField; +import graphql.execution.MergedSelectionSet; +import graphql.language.NodeUtil; import graphql.language.OperationDefinition; import graphql.language.Selection; +import graphql.schema.GraphQLObjectType; import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; @@ -24,6 +28,7 @@ */ @Internal public class SubscriptionUniqueRootField extends AbstractRule { + private final FieldCollector fieldCollector = new FieldCollector(); public SubscriptionUniqueRootField(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { super(validationContext, validationErrorCollector); } @@ -31,36 +36,37 @@ public SubscriptionUniqueRootField(ValidationContext validationContext, Validati @Override public void checkOperationDefinition(OperationDefinition operationDef) { if (operationDef.getOperation() == SUBSCRIPTION) { + + GraphQLObjectType subscriptionType = getValidationContext().getSchema().getSubscriptionType(); + + FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() + .schema(getValidationContext().getSchema()) + .fragments(NodeUtil.getFragmentsByName(getValidationContext().getDocument())) + .variables(CoercedVariables.emptyVariables().toMap()) + .objectType(subscriptionType) + .graphQLContext(getValidationContext().getGraphQLContext()) + .build(); + + MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDef.getSelectionSet()); List subscriptionSelections = operationDef.getSelectionSet().getSelections(); - if (subscriptionSelections.size() > 1) { + if (fields.size() > 1) { String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", operationDef.getName()); addError(SubscriptionMultipleRootFields, operationDef.getSourceLocation(), message); } else { // Only one item in selection set, size == 1 - Selection rootSelection = subscriptionSelections.get(0); - if (isIntrospectionField(rootSelection)) { - String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootField", operationDef.getName(), ((Field) rootSelection).getName()); - addError(SubscriptionIntrospectionRootField, rootSelection.getSourceLocation(), message); - } else if (rootSelection instanceof FragmentSpread) { - // If the only item in selection set is a fragment, inspect the fragment. - String fragmentName = ((FragmentSpread) rootSelection).getName(); - FragmentDefinition fragmentDef = getValidationContext().getFragment(fragmentName); - List fragmentSelections = fragmentDef.getSelectionSet().getSelections(); + MergedField mergedField = fields.getSubFieldsList().get(0); + - if (fragmentSelections.size() > 1) { - String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFieldsWithFragment", operationDef.getName()); - addError(SubscriptionMultipleRootFields, rootSelection.getSourceLocation(), message); - } else if (isIntrospectionField(fragmentSelections.get(0))) { - String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment", operationDef.getName(), ((Field) fragmentSelections.get(0)).getName()); - addError(SubscriptionIntrospectionRootField, rootSelection.getSourceLocation(), message); - } + if (isIntrospectionField(mergedField)) { + String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootField", operationDef.getName(), mergedField.getName()); + addError(SubscriptionIntrospectionRootField, mergedField.getSingleField().getSourceLocation(), message); } } } } - private boolean isIntrospectionField(Selection selection) { - return selection instanceof Field && ((Field) selection).getName().startsWith("__"); + private boolean isIntrospectionField(MergedField field) { + return field.getName().startsWith("__"); } } diff --git a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy b/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy index 1e2d72756f..9b171f2256 100644 --- a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy +++ b/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy @@ -91,7 +91,7 @@ class SubscriptionUniqueRootFieldTest extends Specification { !validationErrors.empty validationErrors.size() == 1 validationErrors[0].validationErrorType == ValidationErrorType.SubscriptionMultipleRootFields - validationErrors[0].message == "Validation error (SubscriptionMultipleRootFields) : Subscription operation 'whoIsAGoodBoy' must have exactly one root field with fragments" + validationErrors[0].message == "Validation error (SubscriptionMultipleRootFields) : Subscription operation 'whoIsAGoodBoy' must have exactly one root field" } def "5.2.3.1 document can contain multiple operations with different root fields"() { @@ -151,9 +151,141 @@ class SubscriptionUniqueRootFieldTest extends Specification { !validationErrors.empty validationErrors.size() == 1 validationErrors[0].validationErrorType == ValidationErrorType.SubscriptionIntrospectionRootField - validationErrors[0].message == "Validation error (SubscriptionIntrospectionRootField) : Subscription operation 'doggo' fragment root field '__typename' cannot be an introspection field" + validationErrors[0].message == "Validation error (SubscriptionIntrospectionRootField) : Subscription operation 'doggo' root field '__typename' cannot be an introspection field" + } + + def "5.2.3.1 subscription with multiple root fields within inline fragment are not allowed"() { + given: + def subscriptionOneRootWithFragment = ''' + subscription doggo { + ... { + dog { + name + } + cat { + name + } + } + } + ''' + + when: + def validationErrors = validate(subscriptionOneRootWithFragment) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors[0].validationErrorType == ValidationErrorType.SubscriptionMultipleRootFields + validationErrors[0].message == "Validation error (SubscriptionMultipleRootFields) : Subscription operation 'doggo' must have exactly one root field" + } + + + def "5.2.3.1 subscription with more than one root field with multiple fragment fails validation"() { + given: + def subscriptionTwoRootsWithFragment = ''' + fragment doggoRoot on SubscriptionRoot { + ...doggoLevel1 + } + + fragment doggoLevel1 on SubscriptionRoot { + ...doggoLevel2 + } + + fragment doggoLevel2 on SubscriptionRoot { + dog { + name + } + cat { + name + } + } + + subscription whoIsAGoodBoy { + ...doggoRoot + } + ''' + when: + def validationErrors = validate(subscriptionTwoRootsWithFragment) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors[0].validationErrorType == ValidationErrorType.SubscriptionMultipleRootFields + validationErrors[0].message == "Validation error (SubscriptionMultipleRootFields) : Subscription operation 'whoIsAGoodBoy' must have exactly one root field" } + + def "5.2.3.1 subscription with more than one root field with multiple fragment with inline fragments fails validation"() { + given: + def subscriptionTwoRootsWithFragment = ''' + fragment doggoRoot on SubscriptionRoot { + ...doggoLevel1 + } + + fragment doggoLevel1 on SubscriptionRoot { + ...{ + ...doggoLevel2 + } + } + + fragment doggoLevel2 on SubscriptionRoot { + ...{ + dog { + name + } + cat { + name + } + } + } + + subscription whoIsAGoodBoy { + ...doggoRoot + } + ''' + when: + def validationErrors = validate(subscriptionTwoRootsWithFragment) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors[0].validationErrorType == ValidationErrorType.SubscriptionMultipleRootFields + validationErrors[0].message == "Validation error (SubscriptionMultipleRootFields) : Subscription operation 'whoIsAGoodBoy' must have exactly one root field" + } + + + def "5.2.3.1 subscription with one root field with multiple fragment with inline fragments does not fail validation"() { + given: + def subscriptionTwoRootsWithFragment = ''' + fragment doggoRoot on SubscriptionRoot { + ...doggoLevel1 + } + + fragment doggoLevel1 on SubscriptionRoot { + ...{ + ...doggoLevel2 + } + } + + fragment doggoLevel2 on SubscriptionRoot { + ...{ + dog { + name + } + + } + } + + subscription whoIsAGoodBoy { + ...doggoRoot + } + ''' + when: + def validationErrors = validate(subscriptionTwoRootsWithFragment) + + then: + validationErrors.empty + } static List validate(String query) { def document = new Parser().parseDocument(query) return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) From e8af0fcd9dd4e8d1c869e68c62e9e40447a3b36f Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 18:43:12 +1100 Subject: [PATCH 264/393] FieldValueInfo was wrong --- src/main/java/graphql/execution/FieldValueInfo.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index 6a5e988633..2d9664619d 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -47,9 +47,12 @@ public CompletableFuture getFieldValueFuture() { return fieldValue; } public Object /* CompletableFuture | Object */ getFieldValueObject() { - return fieldValue; + return fieldValueObject; } + public boolean isFutureValue() { + return fieldValueObject instanceof CompletableFuture; + } public List getFieldValueInfos() { return fieldValueInfos; } From 9e37e3ade66232116639d396d8b3fc6f234a3595 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 19:25:09 +1100 Subject: [PATCH 265/393] null values are now allowed back and hence the assert made less sense --- src/main/java/graphql/execution/Async.java | 2 -- .../groovy/graphql/execution/AsyncTest.groovy | 24 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index ed74f32ef8..d623306220 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -299,7 +299,6 @@ public static CompletableFuture> each(Collection list, Functio for (T t : list) { try { Object value = cfOrMaterialisedValueFactory.apply(t); - Assert.assertNotNull(value, () -> "cfOrMaterialisedValueFactory must return a non null value"); futures.addObject(value); } catch (Exception e) { CompletableFuture cf = new CompletableFuture<>(); @@ -326,7 +325,6 @@ private static void eachSequentiallyPolymorphicImpl(Iterator iterator, Object value; try { value = cfOrMaterialisedValueFactory.apply(iterator.next(), tmpResult); - Assert.assertNotNull(value, () -> "cfOrMaterialisedValueFactory must return a non null value"); } catch (Exception e) { overallResult.completeExceptionally(new CompletionException(e)); return; diff --git a/src/test/groovy/graphql/execution/AsyncTest.groovy b/src/test/groovy/graphql/execution/AsyncTest.groovy index b9e6c2664b..2661c4f5fd 100644 --- a/src/test/groovy/graphql/execution/AsyncTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncTest.groovy @@ -51,7 +51,7 @@ class AsyncTest extends Specification { def "eachSequentially polymorphic test"() { given: - def input = ['a', 'b', 'c'] + def input = ['a', 'b', 'c', 'd'] def cfFactory = Mock(BiFunction) def cf1 = new CompletableFuture() def v2 = 'y' @@ -78,11 +78,12 @@ class AsyncTest extends Specification { 1 * cfFactory.apply('c', ['x', 'y']) >> cf3 when: - cf3.complete('z') + cf3.complete(null) // null valued CFS are allowed then: + 1 * cfFactory.apply('d', ['x', 'y', null]) >> null // nulls are allowed as values result.isDone() - result.get() == ['x', 'y', 'z'] + result.get() == ['x', 'y', null, null] } def "eachSequentially propagates exception"() { @@ -381,6 +382,23 @@ class AsyncTest extends Specification { joinOrMaterialized(awaited) == ["A"] } + def "await polymorphic works as expected with nulls"() { + + when: + def asyncBuilder = Async.ofExpectedSize(5) + asyncBuilder.add(completedFuture("0")) + asyncBuilder.add(completedFuture(null)) + asyncBuilder.addObject("2") + asyncBuilder.addObject(null) + asyncBuilder.add(completedFuture("4")) + + def awaited = asyncBuilder.awaitPolymorphic() + + then: + awaited instanceof CompletableFuture + joinOrMaterialized(awaited) == ["0", null, "2", null, "4"] + } + def "toCompletableFutureOrMaterializedObject tested"() { def x = "x" def cf = completedFuture(x) From 41f925a3b26a41c70203bcbe45ea134ebf2ffff5 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 20:24:58 +1100 Subject: [PATCH 266/393] found a place that could be an object --- src/main/java/graphql/execution/AsyncExecutionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 3e640c9ace..12a663eabf 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -63,7 +63,7 @@ public CompletableFuture execute(ExecutionContext executionCont Async.CombinedBuilder fieldValuesFutures = Async.ofExpectedSize(completeValueInfos.size()); for (FieldValueInfo completeValueInfo : completeValueInfos) { - fieldValuesFutures.add(completeValueInfo.getFieldValueFuture()); + fieldValuesFutures.addObject(completeValueInfo.getFieldValueObject()); } dataLoaderDispatcherStrategy.executionStrategyOnFieldValuesInfo(completeValueInfos, parameters); executionStrategyCtx.onFieldValuesInfo(completeValueInfos); From 6fa38e6f1ff9be9b76514db0e14759a3895212d7 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 21:21:48 +1100 Subject: [PATCH 267/393] using raw value in list code --- src/main/java/graphql/execution/ExecutionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index e9df03988b..c9b767db37 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -759,7 +759,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, index++; } - CompletableFuture> resultsFuture = Async.each(fieldValueInfos, FieldValueInfo::getFieldValueFuture); + CompletableFuture> resultsFuture = Async.each(fieldValueInfos, FieldValueInfo::getFieldValueObject); CompletableFuture overallResult = new CompletableFuture<>(); completeListCtx.onDispatched(); From d890dfb76edf314740a931660d88da15432b3a01 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 29 Feb 2024 22:02:04 +1100 Subject: [PATCH 268/393] now using polymorphic list lookup --- .../graphql/execution/ExecutionStrategy.java | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index c9b767db37..f5f501a675 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -759,24 +759,31 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, index++; } - CompletableFuture> resultsFuture = Async.each(fieldValueInfos, FieldValueInfo::getFieldValueObject); - - CompletableFuture overallResult = new CompletableFuture<>(); - completeListCtx.onDispatched(); - overallResult.whenComplete(completeListCtx::onCompleted); - - resultsFuture.whenComplete((results, exception) -> { - if (exception != null) { - handleValueException(overallResult, exception, executionContext); - return; - } - List completedResults = new ArrayList<>(results.size()); - completedResults.addAll(results); - overallResult.complete(completedResults); - }); - + Object listResults = Async.eachPolymorphic(fieldValueInfos, FieldValueInfo::getFieldValueObject); + Object listOrPromiseToList; + if (listResults instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletableFuture> resultsFuture = (CompletableFuture>) listResults; + CompletableFuture overallResult = new CompletableFuture<>(); + completeListCtx.onDispatched(); + overallResult.whenComplete(completeListCtx::onCompleted); + + resultsFuture.whenComplete((results, exception) -> { + if (exception != null) { + handleValueException(overallResult, exception, executionContext); + return; + } + List completedResults = new ArrayList<>(results.size()); + completedResults.addAll(results); + overallResult.complete(completedResults); + }); + listOrPromiseToList = overallResult; + } else { + completeListCtx.onCompleted(listResults, null); + listOrPromiseToList = listResults; + } return FieldValueInfo.newFieldValueInfo(LIST) - .fieldValueObject(overallResult) + .fieldValueObject(listOrPromiseToList) .fieldValueInfos(fieldValueInfos) .build(); } From 1353cbfdeacb40fd753ccea2d1a2315bef46fcd8 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 1 Mar 2024 07:07:55 +1100 Subject: [PATCH 269/393] Suggestion from Andi --- src/test/java/benchmark/ComplexQueryBenchmark.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index a0160f1065..1dda48e196 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -48,7 +48,7 @@ @State(Scope.Benchmark) @Warmup(iterations = 2, time = 5) @Measurement(iterations = 2) -@Fork(1) +@Fork(2) public class ComplexQueryBenchmark { @Param({"5", "20", "100"}) From bf75d08da6a5a360b6f793c69a50c897d027d3ff Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 1 Mar 2024 09:38:47 +1100 Subject: [PATCH 270/393] Does not allocate a default deferred context that is thrown away on transform --- .../java/graphql/execution/ExecutionStrategyParameters.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index ee0cdce42e..b40fa781c5 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -114,7 +114,7 @@ public static class Builder { ResultPath path = ResultPath.rootPath(); MergedField currentField; ExecutionStrategyParameters parent; - DeferredCallContext deferredCallContext = new DeferredCallContext(); + DeferredCallContext deferredCallContext; /** * @see ExecutionStrategyParameters#newParameters() @@ -188,6 +188,9 @@ public Builder deferredCallContext(DeferredCallContext deferredCallContext) { } public ExecutionStrategyParameters build() { + if (deferredCallContext == null) { + deferredCallContext = new DeferredCallContext(); + } return new ExecutionStrategyParameters(executionStepInfo, source, localContext, fields, nonNullableFieldValidator, path, currentField, parent, deferredCallContext); } } From 2ef1b7be576a17472327efd0cd3d42672a8c4665 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 1 Mar 2024 10:39:47 +1100 Subject: [PATCH 271/393] Now allows the benchmark to go into profiling mode --- .../java/benchmark/ComplexQueryBenchmark.java | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index 1dda48e196..7c35d6473e 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -29,6 +29,9 @@ import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -102,21 +105,47 @@ public static void main(String[] args) throws Exception { @SuppressWarnings({"ConstantValue", "LoopConditionNotUpdatedInsideLoop"}) private static void runAtStartup() { // set this to true if you want to hook in profiler say to a forever running JVM - boolean forever = Boolean.getBoolean("forever"); + int runForMillis = getRunForMillis(); - long then = System.currentTimeMillis(); - System.out.printf("Running initial code before starting the benchmark in forever=%b mode \n", forever); + if (runForMillis <= 0) { + return; + } + long now, then = System.currentTimeMillis(); + System.out.printf("Running initial code before starting the benchmark - runForMillis=%d \n", runForMillis); + System.out.print("Get your profiler in order and press enter... \n"); + readLine(); + System.out.print("Lets go...\n"); ComplexQueryBenchmark complexQueryBenchmark = new ComplexQueryBenchmark(); complexQueryBenchmark.setUp(); do { - System.out.print("Running queries....\n"); + System.out.printf("Running queries for %d millis....\n", System.currentTimeMillis() - then); complexQueryBenchmark.howManyItems = 100; complexQueryBenchmark.runManyQueriesToCompletion(); - } while (forever); + now = System.currentTimeMillis(); + } while ((now - then) < runForMillis); complexQueryBenchmark.tearDown(); System.out.printf("This took %d millis\n", System.currentTimeMillis() - then); + System.exit(0); + + } + + private static void readLine() { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + try { + br.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private static int getRunForMillis() { + String runFor = System.getenv("runForMillis"); + try { + return Integer.parseInt(runFor); + } catch (NumberFormatException e) { + return -1; + } } @SuppressWarnings("UnnecessaryLocalVariable") @@ -200,6 +229,7 @@ private void sleep(Integer howLong) { } AtomicInteger logCount = new AtomicInteger(); + private void logEvery(int every, String s) { int count = logCount.getAndIncrement(); if (count == 0 || count % every == 0) { From d30ea23feacf8a23cf0e03675ceed91a8cf831ed Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 1 Mar 2024 10:47:32 +1100 Subject: [PATCH 272/393] Now allows the benchmark to go into profiling mode - 2 --- src/test/java/benchmark/ComplexQueryBenchmark.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index 7c35d6473e..7224e68404 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -110,11 +110,12 @@ private static void runAtStartup() { if (runForMillis <= 0) { return; } - long now, then = System.currentTimeMillis(); System.out.printf("Running initial code before starting the benchmark - runForMillis=%d \n", runForMillis); System.out.print("Get your profiler in order and press enter... \n"); readLine(); System.out.print("Lets go...\n"); + + long now, then = System.currentTimeMillis(); ComplexQueryBenchmark complexQueryBenchmark = new ComplexQueryBenchmark(); complexQueryBenchmark.setUp(); do { From 26196ca4bc4e072e583b44dad4005e81d6875e16 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 1 Mar 2024 16:23:09 +1100 Subject: [PATCH 273/393] Map benchmarks --- src/test/java/benchmark/MapBenchmark.java | 90 +++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/test/java/benchmark/MapBenchmark.java diff --git a/src/test/java/benchmark/MapBenchmark.java b/src/test/java/benchmark/MapBenchmark.java new file mode 100644 index 0000000000..9ac7fdaa38 --- /dev/null +++ b/src/test/java/benchmark/MapBenchmark.java @@ -0,0 +1,90 @@ +package benchmark; + +import com.google.common.collect.ImmutableMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; + +@State(Scope.Benchmark) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3, batchSize = 1000) +@Fork(3) +public class MapBenchmark { + + int numberEntries = 30; + + Map hashMap; + Map linkedHashMap; + Map immutableMap; + + Random random; + + @Setup(Level.Trial) + public void setUp() { + random = new Random(); + linkedHashMap = new LinkedHashMap<>(); + for (int i = 0; i < numberEntries; i++) { + linkedHashMap.put("string" + i, i); + } + hashMap = new HashMap<>(); + for (int i = 0; i < numberEntries; i++) { + hashMap.put("string" + i, i); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (int i = 0; i < numberEntries; i++) { + builder.put("string" + i, i); + } + immutableMap = builder.build(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void benchmarkLinkedHashMap(Blackhole blackhole) { + mapGet(blackhole, linkedHashMap); + } + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void benchmarkHashMap(Blackhole blackhole) { + mapGet(blackhole, hashMap); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void benchmarkImmutableMap(Blackhole blackhole) { + mapGet(blackhole, immutableMap); + } + + private void mapGet(Blackhole blackhole, Map mapp) { + int index = rand(0, numberEntries); + blackhole.consume(mapp.get("string" + index)); + } + + private int rand(int loInc, int hiExc) { + return random.nextInt(hiExc - loInc) + loInc; + } + + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include("benchmark.MapBenchmark") + .build(); + + new Runner(opt).run(); + } +} + From bf67e9d713fee6687cbb2b72d077d83cae137fb0 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 1 Mar 2024 16:44:59 +1100 Subject: [PATCH 274/393] params --- src/test/java/benchmark/MapBenchmark.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/java/benchmark/MapBenchmark.java b/src/test/java/benchmark/MapBenchmark.java index 9ac7fdaa38..04f05f73f8 100644 --- a/src/test/java/benchmark/MapBenchmark.java +++ b/src/test/java/benchmark/MapBenchmark.java @@ -7,6 +7,7 @@ import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; @@ -22,12 +23,13 @@ import java.util.Random; @State(Scope.Benchmark) -@Warmup(iterations = 2, time = 5) -@Measurement(iterations = 3, batchSize = 1000) +@Warmup(iterations = 2, time = 1) +@Measurement(iterations = 3, time = 1, batchSize = 1000) @Fork(3) public class MapBenchmark { - int numberEntries = 30; + @Param({"10", "50", "300"}) + int numberEntries = 300; Map hashMap; Map linkedHashMap; From 808d7bc24a1ebbbea04b40c48b87b551cd73e132 Mon Sep 17 00:00:00 2001 From: Danny Thomas Date: Fri, 1 Mar 2024 16:52:58 +1100 Subject: [PATCH 275/393] Avoid repeated calls to getFieldDef and unnecessary immutable builders/collections (#3478) * Avoid repeated calls to getFieldDef and unnecessary immutable builders/collections * Remove unnecessary allocations in builders --- .../graphql/execution/ExecutionStrategy.java | 18 +++++++++++++++--- .../ExecutionStrategyParameters.java | 5 ++++- .../graphql/execution/FieldCollector.java | 5 +---- .../graphql/execution/FieldValueInfo.java | 3 ++- .../java/graphql/execution/MergedField.java | 11 +++++++++-- .../graphql/execution/MergedSelectionSet.java | 12 +++++++----- .../graphql/schema/GraphQLInterfaceType.java | 19 +++++++------------ .../graphql/schema/GraphQLObjectType.java | 8 ++++---- src/test/java/benchmark/TwitterBenchmark.java | 16 +++++++++++++--- 9 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 0d79d326b8..8d09d1ebcd 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -348,9 +348,9 @@ protected CompletableFuture resolveFieldWithInfo(ExecutionContex new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() )); - CompletableFuture fetchFieldFuture = fetchField(executionContext, parameters); + CompletableFuture fetchFieldFuture = fetchField(fieldDef, executionContext, parameters); CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> - completeField(executionContext, parameters, fetchedValue)); + completeField(fieldDef, executionContext, parameters, fetchedValue)); CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); @@ -377,7 +377,12 @@ protected CompletableFuture fetchField(ExecutionContext executionC MergedField field = parameters.getField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField()); - GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); + return fetchField(fieldDef, executionContext, parameters); + } + + private CompletableFuture fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + MergedField field = parameters.getField(); + GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); // if the DF (like PropertyDataFetcher) does not use the arguments or execution step info then dont build any @@ -412,6 +417,8 @@ protected CompletableFuture fetchField(ExecutionContext executionC .queryDirectives(queryDirectives) .build(); }); + + GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); DataFetcher dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); Instrumentation instrumentation = executionContext.getInstrumentation(); @@ -555,6 +562,11 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut Field field = parameters.getField().getSingleField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field); + return completeField(fieldDef, executionContext, parameters, fetchedValue); + } + + private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) { + GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); ExecutionStepInfo executionStepInfo = createExecutionStepInfo(executionContext, parameters, fieldDef, parentType); Instrumentation instrumentation = executionContext.getInstrumentation(); diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index ee0cdce42e..b40fa781c5 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -114,7 +114,7 @@ public static class Builder { ResultPath path = ResultPath.rootPath(); MergedField currentField; ExecutionStrategyParameters parent; - DeferredCallContext deferredCallContext = new DeferredCallContext(); + DeferredCallContext deferredCallContext; /** * @see ExecutionStrategyParameters#newParameters() @@ -188,6 +188,9 @@ public Builder deferredCallContext(DeferredCallContext deferredCallContext) { } public ExecutionStrategyParameters build() { + if (deferredCallContext == null) { + deferredCallContext = new DeferredCallContext(); + } return new ExecutionStrategyParameters(executionStepInfo, source, localContext, fields, nonNullableFieldValidator, path, currentField, parent, deferredCallContext); } } diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index 674819407d..93d58f97ab 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -148,10 +148,7 @@ private void collectField(FieldCollectorParameters parameters, Map fieldValueFuture; - private List listInfos = new ArrayList<>(); + private List listInfos = ImmutableList.of(); public Builder(CompleteValueType completeValueType) { this.completeValueType = completeValueType; diff --git a/src/main/java/graphql/execution/MergedField.java b/src/main/java/graphql/execution/MergedField.java index c308b0cc26..e66afb63f8 100644 --- a/src/main/java/graphql/execution/MergedField.java +++ b/src/main/java/graphql/execution/MergedField.java @@ -73,6 +73,10 @@ private MergedField(ImmutableList fields, ImmutableList getFields() { * @return all defer executions. */ @ExperimentalApi - public ImmutableList getDeferredExecutions() { + public List getDeferredExecutions() { return deferredExecutions; } - public static Builder newMergedField() { return new Builder(); } @@ -148,6 +151,10 @@ public static Builder newMergedField(List fields) { return new Builder().fields(fields); } + static MergedField newSingletonMergedField(Field field, DeferredExecution deferredExecution) { + return new MergedField(field, deferredExecution); + } + public MergedField transform(Consumer builderConsumer) { Builder builder = new Builder(this); builderConsumer.accept(builder); diff --git a/src/main/java/graphql/execution/MergedSelectionSet.java b/src/main/java/graphql/execution/MergedSelectionSet.java index a806af8555..8f279f05a6 100644 --- a/src/main/java/graphql/execution/MergedSelectionSet.java +++ b/src/main/java/graphql/execution/MergedSelectionSet.java @@ -13,10 +13,12 @@ @PublicApi public class MergedSelectionSet { - private final ImmutableMap subFields; + private final Map subFields; + private final List keys; protected MergedSelectionSet(Map subFields) { - this.subFields = ImmutableMap.copyOf(Assert.assertNotNull(subFields)); + this.subFields = subFields == null ? ImmutableMap.of() : subFields; + this.keys = ImmutableList.copyOf(this.subFields.keySet()); } public Map getSubFields() { @@ -40,7 +42,7 @@ public MergedField getSubField(String key) { } public List getKeys() { - return ImmutableList.copyOf(keySet()); + return keys; } public boolean isEmpty() { @@ -52,10 +54,10 @@ public static Builder newMergedSelectionSet() { } public static class Builder { - private Map subFields = ImmutableMap.of(); - private Builder() { + private Map subFields; + private Builder() { } public Builder subFields(Map subFields) { diff --git a/src/main/java/graphql/schema/GraphQLInterfaceType.java b/src/main/java/graphql/schema/GraphQLInterfaceType.java index 238b5abdd3..e0159a5ee0 100644 --- a/src/main/java/graphql/schema/GraphQLInterfaceType.java +++ b/src/main/java/graphql/schema/GraphQLInterfaceType.java @@ -2,12 +2,12 @@ import com.google.common.collect.ImmutableList; import graphql.Assert; -import graphql.AssertException; import graphql.DirectivesUtil; import graphql.Internal; import graphql.PublicApi; import graphql.language.InterfaceTypeDefinition; import graphql.language.InterfaceTypeExtensionDefinition; +import graphql.util.FpKit; import graphql.util.TraversalControl; import graphql.util.TraverserContext; @@ -20,12 +20,12 @@ import java.util.function.UnaryOperator; import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; import static graphql.Assert.assertValidName; import static graphql.collect.ImmutableKit.emptyList; import static graphql.schema.GraphqlTypeComparators.sortTypes; import static graphql.util.FpKit.getByName; import static graphql.util.FpKit.valuesToList; -import static java.lang.String.format; /** * In graphql, an interface is an abstract type that defines the set of fields that a type must include to @@ -41,7 +41,7 @@ public class GraphQLInterfaceType implements GraphQLNamedType, GraphQLCompositeT private final String name; private final String description; - private final Map fieldDefinitionsByName = new LinkedHashMap<>(); + private final Map fieldDefinitionsByName; private final TypeResolver typeResolver; private final InterfaceTypeDefinition definition; private final ImmutableList extensionDefinitions; @@ -78,17 +78,12 @@ private GraphQLInterfaceType(String name, this.originalInterfaces = ImmutableList.copyOf(sortTypes(interfaceComparator, interfaces)); this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions); this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); - buildDefinitionMap(fieldDefinitions); + this.fieldDefinitionsByName = buildDefinitionMap(fieldDefinitions); } - private void buildDefinitionMap(List fieldDefinitions) { - for (GraphQLFieldDefinition fieldDefinition : fieldDefinitions) { - String name = fieldDefinition.getName(); - if (fieldDefinitionsByName.containsKey(name)) { - throw new AssertException(format("Duplicated definition for field '%s' in interface '%s'", name, this.name)); - } - fieldDefinitionsByName.put(name, fieldDefinition); - } + private Map buildDefinitionMap(List fieldDefinitions) { + return FpKit.getByName(fieldDefinitions, GraphQLFieldDefinition::getName, + (fld1, fld2) -> assertShouldNeverHappen("Duplicated definition for field '%s' in interface '%s'", fld1.getName(), this.name)); } @Override diff --git a/src/main/java/graphql/schema/GraphQLObjectType.java b/src/main/java/graphql/schema/GraphQLObjectType.java index 732c0ff753..b687215a68 100644 --- a/src/main/java/graphql/schema/GraphQLObjectType.java +++ b/src/main/java/graphql/schema/GraphQLObjectType.java @@ -44,7 +44,7 @@ public class GraphQLObjectType implements GraphQLNamedOutputType, GraphQLComposi private final String name; private final String description; private final Comparator interfaceComparator; - private final ImmutableMap fieldDefinitionsByName; + private final Map fieldDefinitionsByName; private final ImmutableList originalInterfaces; private final DirectivesUtil.DirectivesHolder directivesHolder; private final ObjectTypeDefinition definition; @@ -82,9 +82,9 @@ void replaceInterfaces(List interfaces) { this.replacedInterfaces = ImmutableList.copyOf(sortTypes(interfaceComparator, interfaces)); } - private ImmutableMap buildDefinitionMap(List fieldDefinitions) { - return ImmutableMap.copyOf(FpKit.getByName(fieldDefinitions, GraphQLFieldDefinition::getName, - (fld1, fld2) -> assertShouldNeverHappen("Duplicated definition for field '%s' in type '%s'", fld1.getName(), this.name))); + private Map buildDefinitionMap(List fieldDefinitions) { + return FpKit.getByName(fieldDefinitions, GraphQLFieldDefinition::getName, + (fld1, fld2) -> assertShouldNeverHappen("Duplicated definition for field '%s' in type '%s'", fld1.getName(), this.name)); } @Override diff --git a/src/test/java/benchmark/TwitterBenchmark.java b/src/test/java/benchmark/TwitterBenchmark.java index c32753b7b5..b3d9e34dba 100644 --- a/src/test/java/benchmark/TwitterBenchmark.java +++ b/src/test/java/benchmark/TwitterBenchmark.java @@ -21,6 +21,10 @@ import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.List; @@ -128,8 +132,14 @@ protected Optional getPersistedQueryId(ExecutionInput executionInput) { } } - public static void main(String[] args) { - ExecutionResult result = execute(); - System.out.println(result); + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include("benchmark.TwitterBenchmark") + .forks(1) + .warmupIterations(5) + .measurementIterations(10) + .build(); + + new Runner(opt).run(); } } From daa17608fbab93d4f1d111bc49638b46ec15c614 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 1 Mar 2024 21:10:50 +1100 Subject: [PATCH 276/393] Merged in master again --- .../graphql/execution/ExecutionStrategy.java | 18 +++++++----------- .../java/graphql/execution/FieldValueInfo.java | 7 ++++++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 9d7f1d222f..552ba1426c 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -72,7 +72,6 @@ import static graphql.schema.GraphQLTypeUtil.isEnum; import static graphql.schema.GraphQLTypeUtil.isList; import static graphql.schema.GraphQLTypeUtil.isScalar; -import static java.util.concurrent.CompletableFuture.completedFuture; /** * An execution strategy is give a list of fields from the graphql query to execute and find values for using a recursive strategy. @@ -633,10 +632,10 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut return completeValueForList(executionContext, parameters, result); } else if (isScalar(fieldType)) { fieldValue = completeValueForScalar(executionContext, parameters, (GraphQLScalarType) fieldType, result); - return FieldValueInfo.newFieldValueInfo(SCALAR).fieldValueObject(fieldValue).build(); + return new FieldValueInfo(SCALAR, fieldValue); } else if (isEnum(fieldType)) { fieldValue = completeValueForEnum(executionContext, parameters, (GraphQLEnumType) fieldType, result); - return FieldValueInfo.newFieldValueInfo(ENUM).fieldValueObject(fieldValue).build(); + return new FieldValueInfo(ENUM, fieldValue); } // when we are here, we have a complex type: Interface, Union or Object @@ -653,7 +652,7 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut // complete field as null, validating it is nullable return getFieldValueInfoForNull(parameters); } - return FieldValueInfo.newFieldValueInfo(OBJECT).fieldValueObject(fieldValue).build(); + return new FieldValueInfo(OBJECT, fieldValue); } private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStrategyParameters parameters, UnresolvedTypeException e) { @@ -674,7 +673,7 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra */ private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters parameters) { Object fieldValue = completeValueForNull(parameters); - return FieldValueInfo.newFieldValueInfo(NULL).fieldValueObject(fieldValue).build(); + return new FieldValueInfo(NULL, fieldValue); } protected Object /* CompletableFuture | Object */ completeValueForNull(ExecutionStrategyParameters parameters) { @@ -700,10 +699,10 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, try { resultIterable = parameters.getNonNullFieldValidator().validate(parameters.getPath(), resultIterable); } catch (NonNullableFieldWasNullException e) { - return FieldValueInfo.newFieldValueInfo(LIST).fieldValueObject(exceptionallyCompletedFuture(e)).build(); + return new FieldValueInfo(LIST, exceptionallyCompletedFuture(e)); } if (resultIterable == null) { - return FieldValueInfo.newFieldValueInfo(LIST).fieldValueObject(completedFuture(null)).build(); + return new FieldValueInfo(LIST, null); } return completeValueForList(executionContext, parameters, resultIterable); } @@ -775,10 +774,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, completeListCtx.onCompleted(listResults, null); listOrPromiseToList = listResults; } - return FieldValueInfo.newFieldValueInfo(LIST) - .fieldValueObject(listOrPromiseToList) - .fieldValueInfos(fieldValueInfos) - .build(); + return new FieldValueInfo(LIST, listOrPromiseToList, fieldValueInfos); } protected void handleValueException(CompletableFuture overallResult, Throwable e, ExecutionContext executionContext) { diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index 115e65f0dd..37e2e543f8 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -27,7 +27,11 @@ public enum CompleteValueType { private final Object /* CompletableFuture | Object */ fieldValueObject; private final List fieldValueInfos; - private FieldValueInfo(CompleteValueType completeValueType, Object fieldValueObject, List fieldValueInfos) { + public FieldValueInfo(CompleteValueType completeValueType, Object fieldValueObject) { + this(completeValueType, fieldValueObject, ImmutableList.of()); + } + + public FieldValueInfo(CompleteValueType completeValueType, Object fieldValueObject, List fieldValueInfos) { assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValueObject = fieldValueObject; @@ -55,6 +59,7 @@ public CompletableFuture getFieldValueFuture() { public boolean isFutureValue() { return fieldValueObject instanceof CompletableFuture; } + public List getFieldValueInfos() { return fieldValueInfos; } From 1e725110bdea49e3aa4a911fb33c951df4fdff39 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 13:04:56 +1100 Subject: [PATCH 277/393] Added polymorphic waiting on objects --- .../graphql/execution/ExecutionStrategy.java | 99 ++++++++++++------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 552ba1426c..2200515415 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -194,7 +194,8 @@ public static String mkNameForPath(List currentField) { * * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value */ - protected CompletableFuture> executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + @SuppressWarnings("unchecked") + protected Object /* CompletableFuture> | Map */ executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = executionContext.getDataLoaderDispatcherStrategy(); dataLoaderDispatcherStrategy.executeObject(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); @@ -210,36 +211,63 @@ protected CompletableFuture> executeObject(ExecutionContext Async.CombinedBuilder resolvedFieldFutures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport); CompletableFuture> overallResult = new CompletableFuture<>(); + List fieldsExecutedOnInitialResult = deferredExecutionSupport.getNonDeferredFieldNames(fieldNames); + BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldsExecutedOnInitialResult, overallResult, executionContext); + resolveObjectCtx.onDispatched(); - resolvedFieldFutures.await().whenComplete((completeValueInfos, throwable) -> { - List fieldsExecutedOnInitialResult = deferredExecutionSupport.getNonDeferredFieldNames(fieldNames); + Object fieldValueInfosResult = resolvedFieldFutures.awaitPolymorphic(); + if (fieldValueInfosResult instanceof CompletableFuture) { + CompletableFuture> fieldValueInfos = (CompletableFuture>) fieldValueInfosResult; + fieldValueInfos.whenComplete((completeValueInfos, throwable) -> { + if (throwable != null) { + handleResultsConsumer.accept(null, throwable); + return; + } - BiConsumer, Throwable> handleResultsConsumer = buildFieldValueMap(fieldsExecutedOnInitialResult, overallResult, executionContext); - if (throwable != null) { - handleResultsConsumer.accept(null, throwable); - return; - } + Async.CombinedBuilder resultFutures = fieldValuesCombinedBuilder(completeValueInfos); + dataLoaderDispatcherStrategy.executeObjectOnFieldValuesInfo(completeValueInfos, parameters); + resolveObjectCtx.onFieldValuesInfo(completeValueInfos); + resultFutures.await().whenComplete(handleResultsConsumer); + }).exceptionally((ex) -> { + // if there are any issues with combining/handling the field results, + // complete the future at all costs and bubble up any thrown exception so + // the execution does not hang. + dataLoaderDispatcherStrategy.executeObjectOnFieldValuesException(ex, parameters); + resolveObjectCtx.onFieldValuesException(); + overallResult.completeExceptionally(ex); + return null; + }); + overallResult.whenComplete(resolveObjectCtx::onCompleted); + return overallResult; + } else { + List completeValueInfos = (List) fieldValueInfosResult; - Async.CombinedBuilder resultFutures = Async.ofExpectedSize(completeValueInfos.size()); - for (FieldValueInfo completeValueInfo : completeValueInfos) { - resultFutures.add(completeValueInfo.getFieldValueFuture()); - } + Async.CombinedBuilder resultFutures = fieldValuesCombinedBuilder(completeValueInfos); dataLoaderDispatcherStrategy.executeObjectOnFieldValuesInfo(completeValueInfos, parameters); resolveObjectCtx.onFieldValuesInfo(completeValueInfos); - resultFutures.await().whenComplete(handleResultsConsumer); - }).exceptionally((ex) -> { - // if there are any issues with combining/handling the field results, - // complete the future at all costs and bubble up any thrown exception so - // the execution does not hang. - dataLoaderDispatcherStrategy.executeObjectOnFieldValuesException(ex, parameters); - resolveObjectCtx.onFieldValuesException(); - overallResult.completeExceptionally(ex); - return null; - }); - overallResult.whenComplete(resolveObjectCtx::onCompleted); - return overallResult; + Object completedValuesObject = resultFutures.awaitPolymorphic(); + if (completedValuesObject instanceof CompletableFuture) { + CompletableFuture> completedValues = (CompletableFuture>) completedValuesObject; + completedValues.whenComplete(handleResultsConsumer); + overallResult.whenComplete(resolveObjectCtx::onCompleted); + return overallResult; + } else { + Map fieldValueMap = buildFieldValueMap(fieldsExecutedOnInitialResult, (List) completedValuesObject); + resolveObjectCtx.onCompleted(fieldValueMap,null); + return fieldValueMap; + } + } + } + + @NotNull + private static Async.CombinedBuilder fieldValuesCombinedBuilder(List completeValueInfos) { + Async.CombinedBuilder resultFutures = Async.ofExpectedSize(completeValueInfos.size()); + for (FieldValueInfo completeValueInfo : completeValueInfos) { + resultFutures.addObject(completeValueInfo.getFieldValueObject()); + } + return resultFutures; } private BiConsumer, Throwable> buildFieldValueMap(List fieldNames, CompletableFuture> overallResult, ExecutionContext executionContext) { @@ -248,16 +276,22 @@ private BiConsumer, Throwable> buildFieldValueMap(List fiel handleValueException(overallResult, exception, executionContext); return; } - Map resolvedValuesByField = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size()); - int ix = 0; - for (Object fieldValue : results) { - String fieldName = fieldNames.get(ix++); - resolvedValuesByField.put(fieldName, fieldValue); - } + Map resolvedValuesByField = buildFieldValueMap(fieldNames, results); overallResult.complete(resolvedValuesByField); }; } + @NotNull + private static Map buildFieldValueMap(List fieldNames, List results) { + Map resolvedValuesByField = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size()); + int ix = 0; + for (Object fieldValue : results) { + String fieldName = fieldNames.get(ix++); + resolvedValuesByField.put(fieldName, fieldValue); + } + return resolvedValuesByField; + } + DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { MergedSelectionSet fields = parameters.getFields(); @@ -644,8 +678,7 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut GraphQLObjectType resolvedObjectType; try { resolvedObjectType = resolveType(executionContext, parameters, fieldType); - CompletableFuture> objectValue = completeValueForObject(executionContext, parameters, resolvedObjectType, result); - fieldValue = objectValue.thenApply(map -> map); + fieldValue = completeValueForObject(executionContext, parameters, resolvedObjectType, result); } catch (UnresolvedTypeException ex) { // consider the result to be null and add the error on the context handleUnresolvedTypeProblem(executionContext, parameters, ex); @@ -861,7 +894,7 @@ protected void handleValueException(CompletableFuture overallResult, Thro * * @return a promise to an {@link ExecutionResult} */ - protected CompletableFuture> completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { + protected Object /* CompletableFuture> | Map */ completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); FieldCollectorParameters collectorParameters = newParameters() From 02dad473c23a7f56d3a9ebc130d575b325db854d Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 13:33:45 +1100 Subject: [PATCH 278/393] Dont eagerly create fieldinfo future --- src/main/java/graphql/execution/ExecutionStrategy.java | 4 +--- src/main/java/graphql/execution/FieldValueInfo.java | 8 +++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 552ba1426c..bf8f47920c 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -365,10 +365,8 @@ Async.CombinedBuilder getAsyncFieldValueInfo( CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> completeField(executionContext, parameters, fetchedValue)); - CompletableFuture fieldValueFuture = result.thenCompose(FieldValueInfo::getFieldValueFuture); - fieldCtx.onDispatched(); - fieldValueFuture.whenComplete(fieldCtx::onCompleted); + result.whenComplete(fieldCtx::onCompleted); return result; } else { try { diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index 37e2e543f8..d72657b354 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -23,7 +23,6 @@ public enum CompleteValueType { } private final CompleteValueType completeValueType; - private final CompletableFuture fieldValue; private final Object /* CompletableFuture | Object */ fieldValueObject; private final List fieldValueInfos; @@ -35,7 +34,6 @@ public FieldValueInfo(CompleteValueType completeValueType, Object fieldValueObje assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValueObject = fieldValueObject; - this.fieldValue = Async.toCompletableFuture(fieldValueObject); this.fieldValueInfos = fieldValueInfos; } @@ -45,11 +43,11 @@ public CompleteValueType getCompleteValueType() { @Deprecated(since = "2023-09-11") public CompletableFuture getFieldValue() { - return fieldValue.thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()); + return getFieldValueFuture().thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()); } public CompletableFuture getFieldValueFuture() { - return fieldValue; + return Async.toCompletableFuture(fieldValueObject); } public Object /* CompletableFuture | Object */ getFieldValueObject() { @@ -69,7 +67,7 @@ public List getFieldValueInfos() { public String toString() { return "FieldValueInfo{" + "completeValueType=" + completeValueType + - ", fieldValue=" + fieldValue + + ", fieldValueObject=" + fieldValueObject + ", fieldValueInfos=" + fieldValueInfos + '}'; } From 127f6a16bcc5f8635e09a9b9fed34026aa51f453 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 15:16:38 +1100 Subject: [PATCH 279/393] Updated helper code for profiler attachment --- src/test/java/benchmark/BenchmarkUtils.java | 51 +++++++++++++++++++ .../java/benchmark/ComplexQueryBenchmark.java | 46 +++-------------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/test/java/benchmark/BenchmarkUtils.java b/src/test/java/benchmark/BenchmarkUtils.java index cf92a1396d..c8dda196d0 100644 --- a/src/test/java/benchmark/BenchmarkUtils.java +++ b/src/test/java/benchmark/BenchmarkUtils.java @@ -1,8 +1,13 @@ package benchmark; +import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.concurrent.Callable; public class BenchmarkUtils { @@ -30,4 +35,50 @@ static T asRTE(Callable callable) { } } + public static void runInToolingForSomeTimeThenExit(Runnable setup, Runnable r, Runnable tearDown) { + int runForMillis = getRunForMillis(); + if (runForMillis <= 0) { + System.out.print("'runForMillis' environment var is not set - continuing \n"); + return; + } + System.out.printf("Running initial code in some tooling - runForMillis=%d \n", runForMillis); + System.out.print("Get your tooling in order and press enter..."); + readLine(); + System.out.print("Lets go...\n"); + setup.run(); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss"); + long now, then = System.currentTimeMillis(); + do { + now = System.currentTimeMillis(); + long msLeft = runForMillis - (now - then); + System.out.printf("\t%s Running in loop... %s ms left\n", dtf.format(LocalDateTime.now()), msLeft); + r.run(); + now = System.currentTimeMillis(); + } while ((now - then) < runForMillis); + + tearDown.run(); + + System.out.printf("This ran for %d millis. Exiting...\n", System.currentTimeMillis() - then); + System.exit(0); + } + + private static void readLine() { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + try { + br.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static int getRunForMillis() { + String runFor = System.getenv("runForMillis"); + try { + return Integer.parseInt(runFor); + } catch (NumberFormatException e) { + return -1; + } + } + } diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index 7224e68404..fa552fa226 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -104,50 +104,20 @@ public static void main(String[] args) throws Exception { @SuppressWarnings({"ConstantValue", "LoopConditionNotUpdatedInsideLoop"}) private static void runAtStartup() { - // set this to true if you want to hook in profiler say to a forever running JVM - int runForMillis = getRunForMillis(); - if (runForMillis <= 0) { - return; - } - System.out.printf("Running initial code before starting the benchmark - runForMillis=%d \n", runForMillis); - System.out.print("Get your profiler in order and press enter... \n"); - readLine(); - System.out.print("Lets go...\n"); - - long now, then = System.currentTimeMillis(); ComplexQueryBenchmark complexQueryBenchmark = new ComplexQueryBenchmark(); - complexQueryBenchmark.setUp(); - do { - System.out.printf("Running queries for %d millis....\n", System.currentTimeMillis() - then); - complexQueryBenchmark.howManyItems = 100; - complexQueryBenchmark.runManyQueriesToCompletion(); - now = System.currentTimeMillis(); - } while ((now - then) < runForMillis); - complexQueryBenchmark.tearDown(); - - System.out.printf("This took %d millis\n", System.currentTimeMillis() - then); - System.exit(0); + complexQueryBenchmark.howManyQueries = 5; + complexQueryBenchmark.howManyItems = 10; - } + BenchmarkUtils.runInToolingForSomeTimeThenExit( + complexQueryBenchmark::setUp, + complexQueryBenchmark::runManyQueriesToCompletion, + complexQueryBenchmark::tearDown - private static void readLine() { - BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); - try { - br.readLine(); - } catch (IOException e) { - throw new RuntimeException(e); - } + ); } - private static int getRunForMillis() { - String runFor = System.getenv("runForMillis"); - try { - return Integer.parseInt(runFor); - } catch (NumberFormatException e) { - return -1; - } - } + @SuppressWarnings("UnnecessaryLocalVariable") private Void runManyQueriesToCompletion() { From 901e66481110374f0858cc6d39dc4a4c5fedaec1 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 17:25:05 +1100 Subject: [PATCH 280/393] Raw benchmark on traversing a CF list of objects to traversing a tree of objects --- .../CompletableFuturesBenchmark.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/test/java/benchmark/CompletableFuturesBenchmark.java diff --git a/src/test/java/benchmark/CompletableFuturesBenchmark.java b/src/test/java/benchmark/CompletableFuturesBenchmark.java new file mode 100644 index 0000000000..d49d8e1b24 --- /dev/null +++ b/src/test/java/benchmark/CompletableFuturesBenchmark.java @@ -0,0 +1,109 @@ +package benchmark; + +import com.google.common.collect.ImmutableList; +import graphql.execution.Async; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@State(Scope.Benchmark) +@Warmup(iterations = 2, time = 1) +@Measurement(iterations = 3, time = 10, batchSize = 10) +@Fork(3) +public class CompletableFuturesBenchmark { + + + @Param({"2", "5"}) + public int depth; + public int howMany = 10; + @Setup(Level.Trial) + public void setUp() { + } + + private List> mkCFObjects(int howMany, int depth) { + if (depth <= 0) { + return Collections.emptyList(); + } + ImmutableList.Builder> builder = ImmutableList.builder(); + for (int i = 0; i < howMany; i++) { + CompletableFuture cf = CompletableFuture.completedFuture(mkCFObjects(howMany, depth - 1)); + builder.add(cf); + } + return builder.build(); + } + + private List mkObjects(int howMany, int depth) { + if (depth <= 0) { + return Collections.emptyList(); + } + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < howMany; i++) { + Object obj = mkObjects(howMany, depth - 1); + builder.add(obj); + } + return builder.build(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void benchmarkCFApproach() { + // make results + List> completableFutures = mkCFObjects(howMany, depth); + // traverse results + traverseCFS(completableFutures); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void benchmarkMaterializedApproach() { + // make results + List objects = mkObjects(howMany, depth); + // traverse results + traverseObjects(objects); + } + + @SuppressWarnings("unchecked") + private void traverseCFS(List> completableFutures) { + for (CompletableFuture completableFuture : completableFutures) { + // and when it's done - visit its child results - which are always immediate on completed CFs + // so this whenComplete executed now + completableFuture.whenComplete((list, t) -> { + List> cfs = (List>) list; + traverseCFS(cfs); + }); + } + } + + @SuppressWarnings("unchecked") + private void traverseObjects(List objects) { + for (Object object : objects) { + List list = (List) object; + traverseObjects(list); + } + } + + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include("benchmark.CompletableFuturesBenchmark") + .forks(1) + .build(); + + new Runner(opt).run(); + } + +} From 2c3cda814eb21937c229972054b52a408e846009 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 17:25:24 +1100 Subject: [PATCH 281/393] Raw benchmark on traversing a CF list of objects to traversing a tree of objects- tweak imports --- src/test/java/benchmark/CompletableFuturesBenchmark.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/benchmark/CompletableFuturesBenchmark.java b/src/test/java/benchmark/CompletableFuturesBenchmark.java index d49d8e1b24..d7e06edb64 100644 --- a/src/test/java/benchmark/CompletableFuturesBenchmark.java +++ b/src/test/java/benchmark/CompletableFuturesBenchmark.java @@ -1,7 +1,6 @@ package benchmark; import com.google.common.collect.ImmutableList; -import graphql.execution.Async; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -31,6 +30,7 @@ public class CompletableFuturesBenchmark { @Param({"2", "5"}) public int depth; public int howMany = 10; + @Setup(Level.Trial) public void setUp() { } From 99115a043d912ae8f1716f9074daffc256564f4e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 17:39:26 +1100 Subject: [PATCH 282/393] Raw benchmark on traversing a CF list of objects to traversing a tree of objects- desnt need forks --- src/test/java/benchmark/CompletableFuturesBenchmark.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/benchmark/CompletableFuturesBenchmark.java b/src/test/java/benchmark/CompletableFuturesBenchmark.java index d7e06edb64..e22bb1eb7f 100644 --- a/src/test/java/benchmark/CompletableFuturesBenchmark.java +++ b/src/test/java/benchmark/CompletableFuturesBenchmark.java @@ -100,7 +100,6 @@ private void traverseObjects(List objects) { public static void main(String[] args) throws Exception { Options opt = new OptionsBuilder() .include("benchmark.CompletableFuturesBenchmark") - .forks(1) .build(); new Runner(opt).run(); From da9e9ab170178759ee462bc8b2616699cdcac876 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 22:44:03 +1100 Subject: [PATCH 283/393] Made addObject non-generic for easy of use when polymorphic values come back --- src/main/java/graphql/execution/Async.java | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index d623306220..45e8abbffc 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -25,7 +25,7 @@ public class Async { /** * A builder of materialized objects or {@link CompletableFuture}s than can present a promise to the list of them *

- * This builder has a strict contract on size whereby if the expectedSize is 5 then there MUST be five elements presented to it. + * This builder has a strict contract on size whereby if the expectedSize is five, then there MUST be five elements presented to it. * * @param for two */ @@ -39,11 +39,11 @@ public interface CombinedBuilder { void add(CompletableFuture completableFuture); /** - * This can be either a materialized value or a {@link CompletableFuture} + * This adds a new value which can be either a materialized value or a {@link CompletableFuture} * - * @param objectT the object to add + * @param object the object to add */ - void addObject(T objectT); + void addObject(Object object); /** * This will return a {@code CompletableFuture>} even if the inputs are all materialized values @@ -89,7 +89,7 @@ public void add(CompletableFuture completableFuture) { } @Override - public void addObject(T objectT) { + public void addObject(Object object) { this.ix++; } @@ -127,8 +127,8 @@ public void add(CompletableFuture completableFuture) { } @Override - public void addObject(T objectT) { - this.value = objectT; + public void addObject(Object object) { + this.value = object; this.ix++; } @@ -180,9 +180,9 @@ public void add(CompletableFuture completableFuture) { } @Override - public void addObject(T objectT) { - array[ix++] = objectT; - if (objectT instanceof CompletableFuture) { + public void addObject(Object object) { + array[ix++] = object; + if (object instanceof CompletableFuture) { cfCount++; } } @@ -364,12 +364,12 @@ public static CompletableFuture toCompletableFuture(T t) { } /** - * Turns a= CompletionStage into a CompletableFuture if it's not already, otherwise leaves it alone - * as a materialized object + * Turns a CompletionStage into a CompletableFuture if it's not already, otherwise leaves it alone + * as a materialized object. * * @param object - the object to check * - * @return a CompletableFuture from a CompletionStage or the object itself + * @return a CompletableFuture from a CompletionStage or the materialized object itself */ public static Object toCompletableFutureOrMaterializedObject(Object object) { if (object instanceof CompletionStage) { @@ -396,7 +396,7 @@ public static CompletableFuture exceptionallyCompletedFuture(Throwable ex } /** - * If the passed in CompletableFuture is null then it creates a CompletableFuture that resolves to null + * If the passed in CompletableFuture is null, then it creates a CompletableFuture that resolves to null * * @param completableFuture the CF to use * @param for two From b8aa8a6347a1d7292f6a7ae9ffdc875d481af089 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 2 Mar 2024 22:47:57 +1100 Subject: [PATCH 284/393] merged in async-on-objects --- src/main/java/graphql/execution/ExecutionStrategy.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index bf8f47920c..479d349d73 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -296,12 +296,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( if (!deferredExecutionSupport.isDeferredField(currentField)) { Object fieldValueInfo = resolveFieldWithInfo(executionContext, newParameters); - if (fieldValueInfo instanceof CompletableFuture) { - //noinspection unchecked - futures.add((CompletableFuture) fieldValueInfo); - } else { - futures.addObject((FieldValueInfo) fieldValueInfo); - } + futures.addObject(fieldValueInfo); } } return futures; From dd4341c5c2226822d57847b4a30051ca7f88fa56 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 3 Mar 2024 17:51:04 +1100 Subject: [PATCH 285/393] Created a runner of N benchmarks at once --- .../java/benchmark/ComplexQueryBenchmark.java | 4 +- .../benchmark/IntrospectionBenchmark.java | 110 ++++-------------- .../QueryExecutionOrientedBenchmarks.java | 22 ++++ src/test/java/benchmark/TwitterBenchmark.java | 21 +++- 4 files changed, 61 insertions(+), 96 deletions(-) create mode 100644 src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java diff --git a/src/test/java/benchmark/ComplexQueryBenchmark.java b/src/test/java/benchmark/ComplexQueryBenchmark.java index fa552fa226..530bf7b4aa 100644 --- a/src/test/java/benchmark/ComplexQueryBenchmark.java +++ b/src/test/java/benchmark/ComplexQueryBenchmark.java @@ -54,10 +54,10 @@ @Fork(2) public class ComplexQueryBenchmark { - @Param({"5", "20", "100"}) + @Param({"5", "10", "20"}) int howManyItems = 5; int howLongToSleep = 5; - int howManyQueries = 50; + int howManyQueries = 10; int howManyQueryThreads = 10; int howManyFetcherThreads = 10; diff --git a/src/test/java/benchmark/IntrospectionBenchmark.java b/src/test/java/benchmark/IntrospectionBenchmark.java index f6daf16d5e..d226d232de 100644 --- a/src/test/java/benchmark/IntrospectionBenchmark.java +++ b/src/test/java/benchmark/IntrospectionBenchmark.java @@ -2,17 +2,9 @@ import graphql.ExecutionResult; import graphql.GraphQL; -import graphql.execution.DataFetcherResult; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.SimplePerformantInstrumentation; -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.introspection.IntrospectionQuery; -import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLNamedType; import graphql.schema.GraphQLSchema; import graphql.schema.idl.SchemaGenerator; -import org.jetbrains.annotations.NotNull; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -21,11 +13,10 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; @State(Scope.Benchmark) @Warmup(iterations = 2, time = 5) @@ -33,95 +24,34 @@ @Fork(3) public class IntrospectionBenchmark { - private final GraphQL graphQL; - private final DFCountingInstrumentation countingInstrumentation = new DFCountingInstrumentation(); - - static class DFCountingInstrumentation extends SimplePerformantInstrumentation { - Map counts = new LinkedHashMap<>(); - Map times = new LinkedHashMap<>(); - - @Override - public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { - return (DataFetcher) env -> { - long then = System.nanoTime(); - Object value = dataFetcher.get(env); - long nanos = System.nanoTime() - then; - DataFetcherResult.Builder result = DataFetcherResult.newResult().data(value); - - String path = env.getExecutionStepInfo().getPath().toString(); - String prevTypePath = env.getLocalContext(); + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public ExecutionResult benchMarkIntrospectionAvgTime() { + return graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY); + } - Object source = env.getSource(); - if (isSchemaTypesFetch(env, source)) { - String typeName = ((GraphQLNamedType) source).getName(); + @Benchmark + @BenchmarkMode(Mode.Throughput) + public ExecutionResult benchMarkIntrospectionThroughput() { + return graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY); + } - String prefix = "/__schema/types[" + typeName + "]"; - result.localContext(prefix); - prevTypePath = prefix; - } - if (prevTypePath != null) { - path = path.replaceAll("/__schema/types\\[.*\\]", prevTypePath); - } - counts.compute(path, (k, v) -> v == null ? 1 : v++); - if (nanos > 200_000) { - times.compute(path, (k, v) -> v == null ? nanos : v + nanos); - } - return result.build(); - }; - } + private final GraphQL graphQL; - private boolean isSchemaTypesFetch(DataFetchingEnvironment env, Object source) { - String parentPath = env.getExecutionStepInfo().getParent().getPath().getPathWithoutListEnd().toString(); - return "/__schema/types".equals(parentPath) && source instanceof GraphQLNamedType; - } - } public IntrospectionBenchmark() { String largeSchema = BenchmarkUtils.loadResource("large-schema-4.graphqls"); GraphQLSchema graphQLSchema = SchemaGenerator.createdMockedSchema(largeSchema); graphQL = GraphQL.newGraphQL(graphQLSchema) - //.instrumentation(countingInstrumentation) .build(); } - public static void main(String[] args) { - IntrospectionBenchmark introspectionBenchmark = new IntrospectionBenchmark(); -// while (true) { -// long then = System.currentTimeMillis(); -// ExecutionResult er = introspectionBenchmark.benchMarkIntrospection(); -// long ms = System.currentTimeMillis() - then; -// System.out.println("Took " + ms + "ms"); -// } - - introspectionBenchmark.benchMarkIntrospection(); - - Map counts = sortByValue(introspectionBenchmark.countingInstrumentation.counts); - Map times = sortByValue(introspectionBenchmark.countingInstrumentation.times); - - System.out.println("Counts"); - counts.forEach((k, v) -> System.out.printf("C %-70s : %020d\n", k, v)); - System.out.println("Times"); - times.forEach((k, v) -> System.out.printf("T %-70s : %020d\n", k, v)); - - - } - - public static > Map sortByValue(Map map) { - List> list = new ArrayList<>(map.entrySet()); - list.sort(Map.Entry.comparingByValue()); - - Map result = new LinkedHashMap<>(); - for (Map.Entry entry : list) { - result.put(entry.getKey(), entry.getValue()); - } - - return result; - } + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include("benchmark.IntrospectionBenchmark") + .build(); - @Benchmark - @BenchmarkMode(Mode.AverageTime) - public ExecutionResult benchMarkIntrospection() { - return graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY); + new Runner(opt).run(); } } diff --git a/src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java b/src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java new file mode 100644 index 0000000000..5f707cb76a --- /dev/null +++ b/src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java @@ -0,0 +1,22 @@ +package benchmark; + +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +/** + * A runner of benchmarks that are whole query runners + */ +public class QueryExecutionOrientedBenchmarks { + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include("benchmark.ComplexQueryBenchmark") + .include("benchmark.IntrospectionBenchmark") + .include("benchmark.TwitterBenchmark") + .build(); + + new Runner(opt).run(); + } +} diff --git a/src/test/java/benchmark/TwitterBenchmark.java b/src/test/java/benchmark/TwitterBenchmark.java index c32753b7b5..9136fff7cb 100644 --- a/src/test/java/benchmark/TwitterBenchmark.java +++ b/src/test/java/benchmark/TwitterBenchmark.java @@ -21,6 +21,10 @@ import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.List; @@ -43,7 +47,13 @@ public class TwitterBenchmark { @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) - public void execute(Blackhole bh) { + public void benchmarkThroughput(Blackhole bh) { + bh.consume(execute()); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void benchmarkAvgTime(Blackhole bh) { bh.consume(execute()); } @@ -128,8 +138,11 @@ protected Optional getPersistedQueryId(ExecutionInput executionInput) { } } - public static void main(String[] args) { - ExecutionResult result = execute(); - System.out.println(result); + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include("benchmark.TwitterBenchmark") + .build(); + + new Runner(opt).run(); } } From 26c61addc62a30772c773f6e24e77819caa671b0 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 3 Mar 2024 17:51:40 +1100 Subject: [PATCH 286/393] Created a runner of N benchmarks at once - doco update --- src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java b/src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java index 5f707cb76a..96166a0f5f 100644 --- a/src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java +++ b/src/test/java/benchmark/QueryExecutionOrientedBenchmarks.java @@ -6,7 +6,8 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; /** - * A runner of benchmarks that are whole query runners + * A runner of benchmarks that are whole query runners and they do + * so from the top of the stack all the way in */ public class QueryExecutionOrientedBenchmarks { From aaa00839f9b4aafed348620ff10372dbc1bfb65c Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 4 Mar 2024 10:01:09 +1100 Subject: [PATCH 287/393] Made the field definition lookup more optimised --- .../graphql/execution/ExecutionStrategy.java | 2 +- .../graphql/introspection/Introspection.java | 46 ++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 0d79d326b8..4059aa2d6d 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -923,7 +923,7 @@ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObjectType parentType, Field field) { - return Introspection.getFieldDef(schema, parentType, field.getName()); + return Introspection.getFieldDefinition(schema, parentType, field.getName()); } /** diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index d496e03702..b884fc84c1 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -702,6 +702,43 @@ public static boolean isIntrospectionTypes(GraphQLNamedType type) { */ public static GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLCompositeType parentType, String fieldName) { + GraphQLFieldDefinition fieldDefinition = getSystemFieldDef(schema, parentType, fieldName); + if (fieldDefinition != null) { + return fieldDefinition; + } + + assertTrue(parentType instanceof GraphQLFieldsContainer, () -> String.format("should not happen : parent type must be an object or interface %s", parentType)); + GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) parentType; + fieldDefinition = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, fieldName); + assertTrue(fieldDefinition != null, () -> String.format("Unknown field '%s' for type %s", fieldName, fieldsContainer.getName())); + return fieldDefinition; + } + + /** + * This will look up a field definition by name, and understand that fields like __typename and __schema are special + * and take precedence in field resolution + * + * @param schema the schema to use + * @param parentType the type of the parent {@link GraphQLFieldsContainer} + * @param fieldName the field to look up + * + * @return a field definition otherwise throws an assertion exception if it's null + */ + public static GraphQLFieldDefinition getFieldDefinition(GraphQLSchema schema, GraphQLFieldsContainer parentType, String fieldName) { + // this method is optimized to look up the most common case first (type for field) and hence suits the hot path of the execution engine + // and as a small benefit does not allocate any assertions unless it completely failed + GraphQLFieldDefinition fieldDefinition = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(parentType, fieldName); + if (fieldDefinition == null) { + // we look up system fields second because they are less likely to be the field in question + fieldDefinition = getSystemFieldDef(schema, parentType, fieldName); + if (fieldDefinition == null) { + Assert.assertShouldNeverHappen(String.format("Unknown field '%s' for type %s", fieldName, parentType.getName())); + } + } + return fieldDefinition; + } + + private static GraphQLFieldDefinition getSystemFieldDef(GraphQLSchema schema, GraphQLCompositeType parentType, String fieldName) { if (schema.getQueryType() == parentType) { if (fieldName.equals(schema.getIntrospectionSchemaFieldDefinition().getName())) { return schema.getIntrospectionSchemaFieldDefinition(); @@ -713,11 +750,6 @@ public static GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLCo if (fieldName.equals(schema.getIntrospectionTypenameFieldDefinition().getName())) { return schema.getIntrospectionTypenameFieldDefinition(); } - - assertTrue(parentType instanceof GraphQLFieldsContainer, () -> String.format("should not happen : parent type must be an object or interface %s", parentType)); - GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) parentType; - GraphQLFieldDefinition fieldDefinition = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, fieldName); - assertTrue(fieldDefinition != null, () -> String.format("Unknown field '%s' for type %s", fieldName, fieldsContainer.getName())); - return fieldDefinition; + return null; } -} +} \ No newline at end of file From 076678237f0af1266267967826bbcd5b8974a9c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:20:26 +0000 Subject: [PATCH 288/393] Bump org.codehaus.groovy:groovy from 3.0.20 to 3.0.21 Bumps [org.codehaus.groovy:groovy](https://github.com/apache/groovy) from 3.0.20 to 3.0.21. - [Commits](https://github.com/apache/groovy/commits) --- updated-dependencies: - dependency-name: org.codehaus.groovy:groovy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 77fc672842..baeea5825f 100644 --- a/build.gradle +++ b/build.gradle @@ -106,8 +106,8 @@ dependencies { implementation 'com.google.guava:guava:' + guavaVersion testImplementation group: 'junit', name: 'junit', version: '4.13.2' testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0' - testImplementation 'org.codehaus.groovy:groovy:3.0.20' - testImplementation 'org.codehaus.groovy:groovy-json:3.0.20' + testImplementation 'org.codehaus.groovy:groovy:3.0.21' + testImplementation 'org.codehaus.groovy:groovy-json:3.0.21' testImplementation 'com.google.code.gson:gson:2.10.1' testImplementation 'org.eclipse.jetty:jetty-server:11.0.20' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' From 2ab2181aa09464763130d500cded73c8f307d020 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 5 Mar 2024 14:42:26 +1100 Subject: [PATCH 289/393] Merged master - tweaked the code a little --- src/main/java/graphql/GraphQL.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 64509368e0..096a4f7e26 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -420,7 +420,7 @@ public CompletableFuture executeAsync(ExecutionInput executionI InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInputWithId, this.graphQLSchema); ExecutionInput instrumentedExecutionInput = instrumentation.instrumentExecutionInput(executionInputWithId, inputInstrumentationParameters, instrumentationState); - InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(instrumentedExecutionInput, this.graphQLSchema, instrumentationState); + InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(instrumentedExecutionInput, this.graphQLSchema); InstrumentationContext executionInstrumentation = nonNullCtx(instrumentation.beginExecution(instrumentationParameters, instrumentationState)); executionInstrumentation.onDispatched(); @@ -523,7 +523,7 @@ private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchem } private List validate(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema, instrumentationState), instrumentationState)); + InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema), instrumentationState)); validationCtx.onDispatched(); Predicate> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true); From bdea4a9116416e7fc60059eb92f718fda6f101ce Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 5 Mar 2024 17:13:38 +1100 Subject: [PATCH 290/393] Merged in master --- src/main/java/graphql/execution/ExecutionStrategy.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index ddde70fcee..10ac18f949 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -366,7 +366,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( } else { try { FetchedValue fetchedValue = (FetchedValue) fetchedValueObj; - FieldValueInfo fieldValueInfo = completeField(executionContext, parameters, fetchedValue); + FieldValueInfo fieldValueInfo = completeField(fieldDef,executionContext, parameters, fetchedValue); fieldCtx.onDispatched(); fieldCtx.onCompleted(fetchedValue.getFetchedValue(), null); return fieldValueInfo; @@ -397,7 +397,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( return fetchField(fieldDef, executionContext, parameters); } - private CompletableFuture fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + private Object /*CompletableFuture | FetchedValue>*/ fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { MergedField field = parameters.getField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); From 063485d13b0467f116deaf4c90cb274e61c5da3b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 5 Mar 2024 20:27:02 +1100 Subject: [PATCH 291/393] Intern field names (#3503) Co-authored-by: Danny Thomas --- src/main/java/graphql/language/Field.java | 5 +++-- .../schema/GraphQLFieldDefinition.java | 3 ++- .../graphql/schema/GraphQLInterfaceType.java | 6 ++--- .../graphql/schema/GraphQLObjectType.java | 2 -- .../java/graphql/util/FieldNameInterner.java | 22 +++++++++++++++++++ 5 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 src/main/java/graphql/util/FieldNameInterner.java diff --git a/src/main/java/graphql/language/Field.java b/src/main/java/graphql/language/Field.java index b034ca6bad..5bb4380e94 100644 --- a/src/main/java/graphql/language/Field.java +++ b/src/main/java/graphql/language/Field.java @@ -6,6 +6,7 @@ import graphql.Internal; import graphql.PublicApi; import graphql.collect.ImmutableKit; +import graphql.util.FieldNameInterner; import graphql.util.TraversalControl; import graphql.util.TraverserContext; @@ -50,8 +51,8 @@ protected Field(String name, IgnoredChars ignoredChars, Map additionalData) { super(sourceLocation, comments, ignoredChars, additionalData); - this.name = name; - this.alias = alias; + this.name = name == null ? null : FieldNameInterner.intern(name); + this.alias = alias == null ? null : FieldNameInterner.intern(alias); this.arguments = ImmutableList.copyOf(arguments); this.directives = ImmutableList.copyOf(directives); this.selectionSet = selectionSet; diff --git a/src/main/java/graphql/schema/GraphQLFieldDefinition.java b/src/main/java/graphql/schema/GraphQLFieldDefinition.java index c717c929a2..d8381acc89 100644 --- a/src/main/java/graphql/schema/GraphQLFieldDefinition.java +++ b/src/main/java/graphql/schema/GraphQLFieldDefinition.java @@ -6,6 +6,7 @@ import graphql.Internal; import graphql.PublicApi; import graphql.language.FieldDefinition; +import graphql.util.FieldNameInterner; import graphql.util.TraversalControl; import graphql.util.TraverserContext; @@ -61,7 +62,7 @@ private GraphQLFieldDefinition(String name, assertValidName(name); assertNotNull(type, () -> "type can't be null"); assertNotNull(arguments, () -> "arguments can't be null"); - this.name = name; + this.name = FieldNameInterner.intern(name); this.description = description; this.originalType = type; this.dataFetcherFactory = dataFetcherFactory; diff --git a/src/main/java/graphql/schema/GraphQLInterfaceType.java b/src/main/java/graphql/schema/GraphQLInterfaceType.java index e0159a5ee0..77803b88de 100644 --- a/src/main/java/graphql/schema/GraphQLInterfaceType.java +++ b/src/main/java/graphql/schema/GraphQLInterfaceType.java @@ -51,7 +51,6 @@ public class GraphQLInterfaceType implements GraphQLNamedType, GraphQLCompositeT private final Comparator interfaceComparator; private ImmutableList replacedInterfaces; - public static final String CHILD_FIELD_DEFINITIONS = "fieldDefinitions"; public static final String CHILD_INTERFACES = "interfaces"; @@ -91,7 +90,6 @@ public GraphQLFieldDefinition getFieldDefinition(String name) { return fieldDefinitionsByName.get(name); } - @Override public List getFieldDefinitions() { return ImmutableList.copyOf(fieldDefinitionsByName.values()); @@ -224,9 +222,9 @@ public GraphQLInterfaceType withNewChildren(SchemaElementChildrenContainer newCh @Override public List getInterfaces() { if (replacedInterfaces != null) { - return ImmutableList.copyOf(replacedInterfaces); + return replacedInterfaces; } - return ImmutableList.copyOf(originalInterfaces); + return originalInterfaces; } void replaceInterfaces(List interfaces) { diff --git a/src/main/java/graphql/schema/GraphQLObjectType.java b/src/main/java/graphql/schema/GraphQLObjectType.java index b687215a68..deafaa8c48 100644 --- a/src/main/java/graphql/schema/GraphQLObjectType.java +++ b/src/main/java/graphql/schema/GraphQLObjectType.java @@ -40,7 +40,6 @@ @PublicApi public class GraphQLObjectType implements GraphQLNamedOutputType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, GraphQLImplementingType { - private final String name; private final String description; private final Comparator interfaceComparator; @@ -132,7 +131,6 @@ public List getFieldDefinitions() { return ImmutableList.copyOf(fieldDefinitionsByName.values()); } - @Override public List getInterfaces() { if (replacedInterfaces != null) { diff --git a/src/main/java/graphql/util/FieldNameInterner.java b/src/main/java/graphql/util/FieldNameInterner.java new file mode 100644 index 0000000000..4b697a41fd --- /dev/null +++ b/src/main/java/graphql/util/FieldNameInterner.java @@ -0,0 +1,22 @@ +package graphql.util; + +import com.google.common.collect.Interner; +import com.google.common.collect.Interners; +import graphql.Internal; + +/** + * Interner allowing object identity based comparison of field names. + */ +@Internal +public class FieldNameInterner { + + private FieldNameInterner() { + } + + public static final Interner INTERNER = Interners.newWeakInterner(); + + public static String intern(String name) { + return INTERNER.intern(name); + } + +} From cc067be48bbc143080eb3eebaace18a208507a78 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 5 Mar 2024 20:44:00 +1100 Subject: [PATCH 292/393] Tweaks to Netflix interning PR - naming etc.. --- src/main/java/graphql/language/Field.java | 6 ++--- .../schema/GraphQLFieldDefinition.java | 4 +-- .../java/graphql/util/FieldNameInterner.java | 22 ---------------- src/main/java/graphql/util/Interning.java | 25 +++++++++++++++++++ 4 files changed, 30 insertions(+), 27 deletions(-) delete mode 100644 src/main/java/graphql/util/FieldNameInterner.java create mode 100644 src/main/java/graphql/util/Interning.java diff --git a/src/main/java/graphql/language/Field.java b/src/main/java/graphql/language/Field.java index 5bb4380e94..b4b0bcd97a 100644 --- a/src/main/java/graphql/language/Field.java +++ b/src/main/java/graphql/language/Field.java @@ -6,7 +6,7 @@ import graphql.Internal; import graphql.PublicApi; import graphql.collect.ImmutableKit; -import graphql.util.FieldNameInterner; +import graphql.util.Interning; import graphql.util.TraversalControl; import graphql.util.TraverserContext; @@ -51,8 +51,8 @@ protected Field(String name, IgnoredChars ignoredChars, Map additionalData) { super(sourceLocation, comments, ignoredChars, additionalData); - this.name = name == null ? null : FieldNameInterner.intern(name); - this.alias = alias == null ? null : FieldNameInterner.intern(alias); + this.name = name == null ? null : Interning.intern(name); + this.alias = alias; this.arguments = ImmutableList.copyOf(arguments); this.directives = ImmutableList.copyOf(directives); this.selectionSet = selectionSet; diff --git a/src/main/java/graphql/schema/GraphQLFieldDefinition.java b/src/main/java/graphql/schema/GraphQLFieldDefinition.java index d8381acc89..2328688d50 100644 --- a/src/main/java/graphql/schema/GraphQLFieldDefinition.java +++ b/src/main/java/graphql/schema/GraphQLFieldDefinition.java @@ -6,7 +6,7 @@ import graphql.Internal; import graphql.PublicApi; import graphql.language.FieldDefinition; -import graphql.util.FieldNameInterner; +import graphql.util.Interning; import graphql.util.TraversalControl; import graphql.util.TraverserContext; @@ -62,7 +62,7 @@ private GraphQLFieldDefinition(String name, assertValidName(name); assertNotNull(type, () -> "type can't be null"); assertNotNull(arguments, () -> "arguments can't be null"); - this.name = FieldNameInterner.intern(name); + this.name = Interning.intern(name); this.description = description; this.originalType = type; this.dataFetcherFactory = dataFetcherFactory; diff --git a/src/main/java/graphql/util/FieldNameInterner.java b/src/main/java/graphql/util/FieldNameInterner.java deleted file mode 100644 index 4b697a41fd..0000000000 --- a/src/main/java/graphql/util/FieldNameInterner.java +++ /dev/null @@ -1,22 +0,0 @@ -package graphql.util; - -import com.google.common.collect.Interner; -import com.google.common.collect.Interners; -import graphql.Internal; - -/** - * Interner allowing object identity based comparison of field names. - */ -@Internal -public class FieldNameInterner { - - private FieldNameInterner() { - } - - public static final Interner INTERNER = Interners.newWeakInterner(); - - public static String intern(String name) { - return INTERNER.intern(name); - } - -} diff --git a/src/main/java/graphql/util/Interning.java b/src/main/java/graphql/util/Interning.java new file mode 100644 index 0000000000..bc51b3ebff --- /dev/null +++ b/src/main/java/graphql/util/Interning.java @@ -0,0 +1,25 @@ +package graphql.util; + +import com.google.common.collect.Interner; +import com.google.common.collect.Interners; +import graphql.Internal; +import org.jetbrains.annotations.NotNull; + +/** + * Interner allowing object-identity comparison of key entities like field names. This is useful on hotspot + * areas like the engine where we look up field names a lot inside maps, and those maps use object identity first + * inside the key lookup code. + */ +@Internal +public class Interning { + + private Interning() { + } + + private static final Interner INTERNER = Interners.newWeakInterner(); + + public static @NotNull String intern(@NotNull String name) { + return INTERNER.intern(name); + } + +} From 07c909e15d29e06c4f77c2c29fbc80be050a416b Mon Sep 17 00:00:00 2001 From: Benjamin Habegger Date: Tue, 5 Mar 2024 19:55:00 +0100 Subject: [PATCH 293/393] Fix printing of union types Closes #3434 --- .../DefaultGraphqlTypeComparatorRegistry.java | 11 +++++--- .../java/graphql/schema/GraphQLTypeUtil.java | 1 + src/test/groovy/graphql/Issue3434.groovy | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/test/groovy/graphql/Issue3434.groovy diff --git a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java index a7c3c42f11..82620bcc1b 100644 --- a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java +++ b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java @@ -10,7 +10,7 @@ import java.util.function.UnaryOperator; import static graphql.Assert.assertNotNull; -import static graphql.schema.GraphQLTypeUtil.unwrapAll; +import static graphql.schema.GraphQLTypeUtil.unwrapAllAs; import static graphql.schema.GraphqlTypeComparatorEnvironment.newEnvironment; /** @@ -33,6 +33,7 @@ public class DefaultGraphqlTypeComparatorRegistry implements GraphqlTypeComparat /** * This orders the schema into a sensible grouped order + * * @return a comparator that allows for sensible grouped order */ public static Comparator sensibleGroupedOrder() { @@ -51,7 +52,11 @@ public static Comparator sensibleGroupedOrder() { private static GraphQLSchemaElement unwrapElement(GraphQLSchemaElement element) { if (element instanceof GraphQLType) { - element = unwrapAll((GraphQLType) element); + GraphQLType castElement = (GraphQLType) element; + // We need to unwrap as GraphQLType to support GraphQLTypeReferences which is not an GraphQLUnmodifiedType + // as returned by unwrapAll. + castElement = unwrapAllAs(castElement); + element = castElement; } return element; } @@ -59,7 +64,7 @@ private static GraphQLSchemaElement unwrapElement(GraphQLSchemaElement element) private static int compareByName(GraphQLSchemaElement o1, GraphQLSchemaElement o2) { return Comparator.comparing(element -> { if (element instanceof GraphQLType) { - element = unwrapAll((GraphQLType) element); + element = unwrapElement((GraphQLType) element); } if (element instanceof GraphQLNamedSchemaElement) { return ((GraphQLNamedSchemaElement) element).getName(); diff --git a/src/main/java/graphql/schema/GraphQLTypeUtil.java b/src/main/java/graphql/schema/GraphQLTypeUtil.java index de5cd5e8b7..c2a44d641c 100644 --- a/src/main/java/graphql/schema/GraphQLTypeUtil.java +++ b/src/main/java/graphql/schema/GraphQLTypeUtil.java @@ -184,6 +184,7 @@ public static T unwrapOneAs(GraphQLType type) { /** * Unwraps all layers of the type or just returns the type again if it's not a wrapped type + * NOTE: This method does not support GraphQLTypeReference as input and will lead to a ClassCastException * * @param type the type to unwrapOne * diff --git a/src/test/groovy/graphql/Issue3434.groovy b/src/test/groovy/graphql/Issue3434.groovy new file mode 100644 index 0000000000..4671c57ff8 --- /dev/null +++ b/src/test/groovy/graphql/Issue3434.groovy @@ -0,0 +1,26 @@ +package graphql + +import static graphql.schema.GraphQLUnionType.newUnionType +import static graphql.schema.GraphQLTypeReference.typeRef +import graphql.schema.idl.SchemaPrinter + +import spock.lang.Specification + +class Issue3434 extends Specification { + + def "allow printing of union types"() { + given: + def schema = newUnionType().name("Shape") + .possibleType(typeRef("Circle")) + .possibleType(typeRef("Square")) + .build() + + when: + def printer = new SchemaPrinter() + def result = printer.print(schema) + + then: + result.trim() == "union Shape = Circle | Square" + } +} + From 7d29e37f28c296ebf178ad035c54449bb77f62fb Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 6 Mar 2024 21:14:31 +1100 Subject: [PATCH 294/393] Cheaper assertions with constant strings --- src/main/java/graphql/Assert.java | 42 ++++++++++++ .../java/graphql/GraphqlErrorBuilder.java | 3 +- src/main/java/graphql/execution/Async.java | 8 +-- .../graphql/execution/ExecutionStepInfo.java | 5 +- .../ExecutionStrategyParameters.java | 6 +- .../execution/FieldCollectorParameters.java | 2 +- .../graphql/execution/FieldValueInfo.java | 3 +- .../java/graphql/execution/ResultPath.java | 15 +++-- .../SubscriptionExecutionStrategy.java | 3 +- .../execution/ValuesResolverConversion.java | 4 +- .../reactive/NonBlockingMutexExecutor.java | 3 +- .../graphql/extensions/ExtensionsBuilder.java | 4 +- .../java/graphql/language/AbstractNode.java | 6 +- .../java/graphql/language/AstPrinter.java | 3 +- .../java/graphql/language/NodeParentTree.java | 5 +- .../graphql/language/PrettyAstPrinter.java | 3 +- .../normalized/ExecutableNormalizedField.java | 4 +- .../normalized/NormalizedInputValue.java | 4 +- .../java/graphql/schema/AsyncDataFetcher.java | 5 +- .../DefaultGraphqlTypeComparatorRegistry.java | 9 ++- .../java/graphql/schema/GraphQLTypeUtil.java | 2 +- .../graphql/schema/InputValueWithState.java | 3 +- src/test/groovy/graphql/AssertTest.groovy | 64 +++++++++++++++++-- 23 files changed, 159 insertions(+), 47 deletions(-) diff --git a/src/main/java/graphql/Assert.java b/src/main/java/graphql/Assert.java index 5fcbf9dbe9..d780e419f3 100644 --- a/src/main/java/graphql/Assert.java +++ b/src/main/java/graphql/Assert.java @@ -31,6 +31,22 @@ public static T assertNotNull(T object) { throw new AssertException("Object required to be not null"); } + /** + * If you use this method, make sure that the message is a constant string + * + * @param object the object to check for null + * @param constantMsg make sure this is a constant message + * @param for two + * + * @return the object + */ + public static T assertNotNull(T object, String constantMsg) { + if (object != null) { + return object; + } + throw new AssertException(constantMsg); + } + public static void assertNull(T object, Supplier msg) { if (object == null) { return; @@ -85,6 +101,19 @@ public static void assertTrue(boolean condition) { throw new AssertException("condition expected to be true"); } + /** + * If you use this method, make sure that the message is a constant string + * + * @param condition the condition that must be true + * @param constantMsg make sure this is a constant message + */ + public static void assertTrue(boolean condition, String constantMsg) { + if (condition) { + return; + } + throw new AssertException(constantMsg); + } + public static void assertFalse(boolean condition, Supplier msg) { if (!condition) { return; @@ -99,6 +128,19 @@ public static void assertFalse(boolean condition) { throw new AssertException("condition expected to be false"); } + /** + * If you use this method, make sure that the message is a constant string + * + * @param condition the condition that must be false + * @param constantMsg make sure this is a constant message + */ + public static void assertFalse(boolean condition, String constantMsg) { + if (!condition) { + return; + } + throw new AssertException(constantMsg); + } + private static final String invalidNameErrorMessage = "Name must be non-null, non-empty and match [_A-Za-z][_0-9A-Za-z]* - was '%s'"; private static final Pattern validNamePattern = Pattern.compile("[_A-Za-z][_0-9A-Za-z]*"); diff --git a/src/main/java/graphql/GraphqlErrorBuilder.java b/src/main/java/graphql/GraphqlErrorBuilder.java index 4cef5beabe..c7281a5f53 100644 --- a/src/main/java/graphql/GraphqlErrorBuilder.java +++ b/src/main/java/graphql/GraphqlErrorBuilder.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; /** @@ -128,7 +129,7 @@ public B extensions(@Nullable Map extensions) { * @return a newly built GraphqlError */ public GraphQLError build() { - assertNotNull(message, () -> "You must provide error message"); + Assert.assertNotNull(message,"You must provide error message"); return new GraphqlErrorImpl(message, locations, errorType, path, extensions); } diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index 56f7a2f9be..cbdd45d96b 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -58,7 +58,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == 0, () -> "expected size was " + 0 + " got " + ix); + Assert.assertTrue(ix == 0, "expected size was 0"); return typedEmpty(); } @@ -109,7 +109,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == array.length, () -> "expected size was " + array.length + " got " + ix); + Assert.assertTrue(ix == array.length, "expected size was less than the array length"); CompletableFuture> overallResult = new CompletableFuture<>(); CompletableFuture.allOf(array) @@ -135,7 +135,7 @@ public static CompletableFuture> each(Collection list, Functio CompletableFuture cf; try { cf = cfFactory.apply(t); - Assert.assertNotNull(cf, () -> "cfFactory must return a non null value"); + Assert.assertNotNull(cf, "cfFactory must return a non null value"); } catch (Exception e) { cf = new CompletableFuture<>(); // Async.each makes sure that it is not a CompletionException inside a CompletionException @@ -160,7 +160,7 @@ private static void eachSequentiallyImpl(Iterator iterator, BiFunction CompletableFuture cf; try { cf = cfFactory.apply(iterator.next(), tmpResult); - Assert.assertNotNull(cf, () -> "cfFactory must return a non null value"); + Assert.assertNotNull(cf,"cfFactory must return a non null value"); } catch (Exception e) { cf = new CompletableFuture<>(); cf.completeExceptionally(new CompletionException(e)); diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index 08836d977f..42e0b01ebb 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfo.java +++ b/src/main/java/graphql/execution/ExecutionStepInfo.java @@ -1,5 +1,6 @@ package graphql.execution; +import graphql.Assert; import graphql.PublicApi; import graphql.collect.ImmutableMapWithNullValues; import graphql.schema.GraphQLFieldDefinition; @@ -72,7 +73,7 @@ private ExecutionStepInfo(Builder builder) { this.field = builder.field; this.path = builder.path; this.parent = builder.parentInfo; - this.type = assertNotNull(builder.type, () -> "you must provide a graphql type"); + this.type = Assert.assertNotNull(builder.type, "you must provide a graphql type"); this.arguments = builder.arguments; this.fieldContainer = builder.fieldContainer; } @@ -202,7 +203,7 @@ public boolean hasParent() { * @return a new type info with the same */ public ExecutionStepInfo changeTypeWithPreservedNonNull(GraphQLOutputType newType) { - assertTrue(!GraphQLTypeUtil.isNonNull(newType), () -> "newType can't be non null"); + Assert.assertTrue(!GraphQLTypeUtil.isNonNull(newType), "newType can't be non null"); if (isNonNullType()) { return newExecutionStepInfo(this).type(GraphQLNonNull.nonNull(newType)).build(); } else { diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index b40fa781c5..4df2b73605 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -33,9 +33,9 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, ExecutionStrategyParameters parent, DeferredCallContext deferredCallContext) { - this.executionStepInfo = assertNotNull(executionStepInfo, () -> "executionStepInfo is null"); + this.executionStepInfo = Assert.assertNotNull(executionStepInfo, "executionStepInfo is null"); this.localContext = localContext; - this.fields = assertNotNull(fields, () -> "fields is null"); + this.fields = Assert.assertNotNull(fields, "fields is null"); this.source = source; this.nonNullableFieldValidator = nonNullableFieldValidator; this.path = path; @@ -168,7 +168,7 @@ public Builder localContext(Object localContext) { } public Builder nonNullFieldValidator(NonNullableFieldValidator nonNullableFieldValidator) { - this.nonNullableFieldValidator = Assert.assertNotNull(nonNullableFieldValidator, () -> "requires a NonNullValidator"); + this.nonNullableFieldValidator = Assert.assertNotNull(nonNullableFieldValidator, "requires a NonNullValidator"); return this; } diff --git a/src/main/java/graphql/execution/FieldCollectorParameters.java b/src/main/java/graphql/execution/FieldCollectorParameters.java index c0a23404a7..75dafc1eff 100644 --- a/src/main/java/graphql/execution/FieldCollectorParameters.java +++ b/src/main/java/graphql/execution/FieldCollectorParameters.java @@ -92,7 +92,7 @@ public Builder variables(Map variables) { } public FieldCollectorParameters build() { - Assert.assertNotNull(graphQLSchema, () -> "You must provide a schema"); + Assert.assertNotNull(graphQLSchema, "You must provide a schema"); return new FieldCollectorParameters(this); } diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index c13e915936..a170ecb6e3 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -1,6 +1,7 @@ package graphql.execution; import com.google.common.collect.ImmutableList; +import graphql.Assert; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.PublicApi; @@ -31,7 +32,7 @@ public enum CompleteValueType { } FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { - assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); + Assert.assertNotNull(fieldValueInfos, "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValue = fieldValue; this.fieldValueInfos = fieldValueInfos; diff --git a/src/main/java/graphql/execution/ResultPath.java b/src/main/java/graphql/execution/ResultPath.java index 7f65379ade..9544ad57c7 100644 --- a/src/main/java/graphql/execution/ResultPath.java +++ b/src/main/java/graphql/execution/ResultPath.java @@ -12,6 +12,7 @@ import java.util.Objects; import java.util.StringTokenizer; +import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; import static graphql.Assert.assertTrue; import static java.lang.String.format; @@ -46,12 +47,12 @@ private ResultPath() { } private ResultPath(ResultPath parent, String segment) { - this.parent = assertNotNull(parent, () -> "Must provide a parent path"); - this.segment = assertNotNull(segment, () -> "Must provide a sub path"); + this.parent = Assert.assertNotNull(parent, "Must provide a parent path"); + this.segment = Assert.assertNotNull(segment, "Must provide a sub path"); } private ResultPath(ResultPath parent, int segment) { - this.parent = assertNotNull(parent, () -> "Must provide a parent path"); + this.parent = Assert.assertNotNull(parent, "Must provide a parent path"); this.segment = segment; } @@ -199,7 +200,7 @@ public ResultPath dropSegment() { * @return a new path with the last segment replaced */ public ResultPath replaceSegment(int segment) { - Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); + Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); return new ResultPath(parent, segment); } @@ -211,7 +212,7 @@ public ResultPath replaceSegment(int segment) { * @return a new path with the last segment replaced */ public ResultPath replaceSegment(String segment) { - Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); + Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); return new ResultPath(parent, segment); } @@ -237,12 +238,12 @@ public ResultPath append(ResultPath path) { public ResultPath sibling(String siblingField) { - Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); + Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); return new ResultPath(this.parent, siblingField); } public ResultPath sibling(int siblingField) { - Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); + Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); return new ResultPath(this.parent, siblingField); } diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index f735b0dc6a..16a91a942b 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -1,5 +1,6 @@ package graphql.execution; +import graphql.Assert; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.PublicApi; @@ -97,7 +98,7 @@ private CompletableFuture> createSourceEventStream(ExecutionCo return fieldFetched.thenApply(fetchedValue -> { Object publisher = fetchedValue.getFetchedValue(); if (publisher != null) { - assertTrue(publisher instanceof Publisher, () -> "Your data fetcher must return a Publisher of events when using graphql subscriptions"); + Assert.assertTrue(publisher instanceof Publisher, "Your data fetcher must return a Publisher of events when using graphql subscriptions"); } //noinspection unchecked return (Publisher) publisher; diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index 29c3edaf2d..a80a0a7358 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -1,6 +1,7 @@ package graphql.execution; import com.google.common.collect.ImmutableList; +import graphql.Assert; import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.values.InputInterceptor; @@ -38,6 +39,7 @@ import static graphql.Assert.assertShouldNeverHappen; import static graphql.Assert.assertTrue; +import static graphql.Assert.assertTrue; import static graphql.collect.ImmutableKit.emptyList; import static graphql.collect.ImmutableKit.map; import static graphql.execution.ValuesResolver.ValueMode.NORMALIZED; @@ -281,7 +283,7 @@ private static Object externalValueToLiteralForObject( GraphQLContext graphqlContext, Locale locale ) { - assertTrue(inputValue instanceof Map, () -> "Expect Map as input"); + Assert.assertTrue(inputValue instanceof Map, "Expect Map as input"); Map inputMap = (Map) inputValue; List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); diff --git a/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java b/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java index 49f6fde6ee..449e65f5d3 100644 --- a/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java +++ b/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java @@ -1,6 +1,7 @@ package graphql.execution.reactive; +import graphql.Assert; import graphql.Internal; import java.util.concurrent.Executor; @@ -37,7 +38,7 @@ class NonBlockingMutexExecutor implements Executor { @Override public void execute(final Runnable command) { - final RunNode newNode = new RunNode(assertNotNull(command, () -> "Runnable must not be null")); + final RunNode newNode = new RunNode(Assert.assertNotNull(command, "Runnable must not be null")); final RunNode prevLast = last.getAndSet(newNode); if (prevLast != null) prevLast.lazySet(newNode); diff --git a/src/main/java/graphql/extensions/ExtensionsBuilder.java b/src/main/java/graphql/extensions/ExtensionsBuilder.java index fe37c64743..8f731cf33b 100644 --- a/src/main/java/graphql/extensions/ExtensionsBuilder.java +++ b/src/main/java/graphql/extensions/ExtensionsBuilder.java @@ -1,6 +1,7 @@ package graphql.extensions; import com.google.common.collect.ImmutableMap; +import graphql.Assert; import graphql.ExecutionResult; import graphql.PublicApi; import org.jetbrains.annotations.NotNull; @@ -12,6 +13,7 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; /** @@ -106,7 +108,7 @@ public Map buildExtensions() { Map outMap = new LinkedHashMap<>(firstChange); for (int i = 1; i < changes.size(); i++) { Map newMap = extensionsMerger.merge(outMap, changes.get(i)); - assertNotNull(outMap, () -> "You MUST provide a non null Map from ExtensionsMerger.merge()"); + Assert.assertNotNull(outMap, "You MUST provide a non null Map from ExtensionsMerger.merge()"); outMap = newMap; } return outMap; diff --git a/src/main/java/graphql/language/AbstractNode.java b/src/main/java/graphql/language/AbstractNode.java index f097c9b491..736d23a98f 100644 --- a/src/main/java/graphql/language/AbstractNode.java +++ b/src/main/java/graphql/language/AbstractNode.java @@ -25,9 +25,9 @@ public AbstractNode(SourceLocation sourceLocation, List comments, Ignor } public AbstractNode(SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { - Assert.assertNotNull(comments, () -> "comments can't be null"); - Assert.assertNotNull(ignoredChars, () -> "ignoredChars can't be null"); - Assert.assertNotNull(additionalData, () -> "additionalData can't be null"); + Assert.assertNotNull(comments, "comments can't be null"); + Assert.assertNotNull(ignoredChars, "ignoredChars can't be null"); + Assert.assertNotNull(additionalData, "additionalData can't be null"); this.sourceLocation = sourceLocation; this.additionalData = ImmutableMap.copyOf(additionalData); diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index 3deff35b25..df2c1f71ea 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -1,5 +1,6 @@ package graphql.language; +import graphql.Assert; import graphql.AssertException; import graphql.PublicApi; import graphql.collect.ImmutableKit; @@ -455,7 +456,7 @@ private String node(Node node) { private String node(Node node, Class startClass) { if (startClass != null) { - assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); + Assert.assertTrue(startClass.isInstance(node), "The starting class must be in the inherit tree"); } StringBuilder builder = new StringBuilder(); NodePrinter printer = _findPrinter(node, startClass); diff --git a/src/main/java/graphql/language/NodeParentTree.java b/src/main/java/graphql/language/NodeParentTree.java index fc78ea093d..616efadacc 100644 --- a/src/main/java/graphql/language/NodeParentTree.java +++ b/src/main/java/graphql/language/NodeParentTree.java @@ -1,6 +1,7 @@ package graphql.language; import com.google.common.collect.ImmutableList; +import graphql.Assert; import graphql.Internal; import graphql.PublicApi; @@ -28,8 +29,8 @@ public class NodeParentTree { @Internal public NodeParentTree(Deque nodeStack) { - assertNotNull(nodeStack, () -> "You MUST have a non null stack of nodes"); - assertTrue(!nodeStack.isEmpty(), () -> "You MUST have a non empty stack of nodes"); + Assert.assertNotNull(nodeStack, "You MUST have a non null stack of nodes"); + Assert.assertTrue(!nodeStack.isEmpty(), "You MUST have a non empty stack of nodes"); Deque copy = new ArrayDeque<>(nodeStack); path = mkPath(copy); diff --git a/src/main/java/graphql/language/PrettyAstPrinter.java b/src/main/java/graphql/language/PrettyAstPrinter.java index 8d29f3864d..2a73b6cd8b 100644 --- a/src/main/java/graphql/language/PrettyAstPrinter.java +++ b/src/main/java/graphql/language/PrettyAstPrinter.java @@ -1,5 +1,6 @@ package graphql.language; +import graphql.Assert; import graphql.ExperimentalApi; import graphql.collect.ImmutableKit; import graphql.parser.CommentParser; @@ -220,7 +221,7 @@ private NodePrinter unionTypeDefinition(String nodeName) { private String node(Node node, Class startClass) { if (startClass != null) { - assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); + Assert.assertTrue(startClass.isInstance(node), "The starting class must be in the inherit tree"); } StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 47f8151229..ebc5a21727 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -184,7 +184,7 @@ public boolean hasChildren() { public GraphQLOutputType getType(GraphQLSchema schema) { List fieldDefinitions = getFieldDefinitions(schema); Set fieldTypes = fieldDefinitions.stream().map(fd -> simplePrint(fd.getType())).collect(toSet()); - Assert.assertTrue(fieldTypes.size() == 1, () -> "More than one type ... use getTypes"); + Assert.assertTrue(fieldTypes.size() == 1, "More than one type ... use getTypes"); return fieldDefinitions.get(0).getType(); } @@ -438,7 +438,7 @@ public List getChildrenWithSameResultKey(String resul public List getChildren(int includingRelativeLevel) { List result = new ArrayList<>(); - assertTrue(includingRelativeLevel >= 1, () -> "relative level must be >= 1"); + Assert.assertTrue(includingRelativeLevel >= 1, "relative level must be >= 1"); this.getChildren().forEach(child -> { traverseImpl(child, result::add, 1, includingRelativeLevel); diff --git a/src/main/java/graphql/normalized/NormalizedInputValue.java b/src/main/java/graphql/normalized/NormalizedInputValue.java index 390bac032a..a73d90c4a9 100644 --- a/src/main/java/graphql/normalized/NormalizedInputValue.java +++ b/src/main/java/graphql/normalized/NormalizedInputValue.java @@ -1,5 +1,6 @@ package graphql.normalized; +import graphql.Assert; import graphql.PublicApi; import graphql.language.Value; @@ -7,6 +8,7 @@ import static graphql.Assert.assertNotNull; import static graphql.Assert.assertTrue; +import static graphql.Assert.assertTrue; import static graphql.Assert.assertValidName; import static graphql.language.AstPrinter.printAst; @@ -99,7 +101,7 @@ private boolean isListOnly(String typeName) { private String unwrapOne(String typeName) { assertNotNull(typeName); - assertTrue(typeName.trim().length() > 0, () -> "We have an empty type name unwrapped"); + Assert.assertTrue(typeName.trim().length() > 0, "We have an empty type name unwrapped"); if (typeName.endsWith("!")) { return typeName.substring(0, typeName.length() - 1); } diff --git a/src/main/java/graphql/schema/AsyncDataFetcher.java b/src/main/java/graphql/schema/AsyncDataFetcher.java index b07aa80ddb..fc405fe4b6 100644 --- a/src/main/java/graphql/schema/AsyncDataFetcher.java +++ b/src/main/java/graphql/schema/AsyncDataFetcher.java @@ -1,5 +1,6 @@ package graphql.schema; +import graphql.Assert; import graphql.PublicApi; import java.util.concurrent.CompletableFuture; @@ -67,8 +68,8 @@ public AsyncDataFetcher(DataFetcher wrappedDataFetcher) { } public AsyncDataFetcher(DataFetcher wrappedDataFetcher, Executor executor) { - this.wrappedDataFetcher = assertNotNull(wrappedDataFetcher, () -> "wrappedDataFetcher can't be null"); - this.executor = assertNotNull(executor, () -> "executor can't be null"); + this.wrappedDataFetcher = Assert.assertNotNull(wrappedDataFetcher, "wrappedDataFetcher can't be null"); + this.executor = Assert.assertNotNull(executor, "executor can't be null"); } @Override diff --git a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java index a7c3c42f11..c945ddf3c9 100644 --- a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java +++ b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java @@ -1,6 +1,7 @@ package graphql.schema; import com.google.common.collect.ImmutableMap; +import graphql.Assert; import graphql.PublicApi; import java.util.Comparator; @@ -9,6 +10,7 @@ import java.util.Objects; import java.util.function.UnaryOperator; +import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; import static graphql.schema.GraphQLTypeUtil.unwrapAll; import static graphql.schema.GraphqlTypeComparatorEnvironment.newEnvironment; @@ -33,6 +35,7 @@ public class DefaultGraphqlTypeComparatorRegistry implements GraphqlTypeComparat /** * This orders the schema into a sensible grouped order + * * @return a comparator that allows for sensible grouped order */ public static Comparator sensibleGroupedOrder() { @@ -124,9 +127,9 @@ public static class Builder { * @return The {@code Builder} instance to allow chaining. */ public Builder addComparator(GraphqlTypeComparatorEnvironment environment, Class comparatorClass, Comparator comparator) { - assertNotNull(environment, () -> "environment can't be null"); - assertNotNull(comparatorClass, () -> "comparatorClass can't be null"); - assertNotNull(comparator, () -> "comparator can't be null"); + Assert.assertNotNull(environment, "environment can't be null"); + Assert.assertNotNull(comparatorClass, "comparatorClass can't be null"); + Assert.assertNotNull(comparator, "comparator can't be null"); registry.put(environment, comparator); return this; } diff --git a/src/main/java/graphql/schema/GraphQLTypeUtil.java b/src/main/java/graphql/schema/GraphQLTypeUtil.java index de5cd5e8b7..f33164937b 100644 --- a/src/main/java/graphql/schema/GraphQLTypeUtil.java +++ b/src/main/java/graphql/schema/GraphQLTypeUtil.java @@ -26,7 +26,7 @@ public class GraphQLTypeUtil { * @return the type in graphql SDL format, eg [typeName!]! */ public static String simplePrint(GraphQLType type) { - Assert.assertNotNull(type, () -> "type can't be null"); + Assert.assertNotNull(type, "type can't be null"); if (isNonNull(type)) { return simplePrint(unwrapOne(type)) + "!"; } else if (isList(type)) { diff --git a/src/main/java/graphql/schema/InputValueWithState.java b/src/main/java/graphql/schema/InputValueWithState.java index 33ef45d062..0a8ed45f0a 100644 --- a/src/main/java/graphql/schema/InputValueWithState.java +++ b/src/main/java/graphql/schema/InputValueWithState.java @@ -1,5 +1,6 @@ package graphql.schema; +import graphql.Assert; import graphql.PublicApi; import graphql.language.Value; import org.jetbrains.annotations.NotNull; @@ -45,7 +46,7 @@ private InputValueWithState(State state, Object value) { public static final InputValueWithState NOT_SET = new InputValueWithState(State.NOT_SET, null); public static InputValueWithState newLiteralValue(@NotNull Value value) { - assertNotNull(value, () -> "value literal can't be null"); + Assert.assertNotNull(value, "value literal can't be null"); return new InputValueWithState(State.LITERAL, value); } diff --git a/src/test/groovy/graphql/AssertTest.groovy b/src/test/groovy/graphql/AssertTest.groovy index 074c8b9b67..13a4bae364 100644 --- a/src/test/groovy/graphql/AssertTest.groovy +++ b/src/test/groovy/graphql/AssertTest.groovy @@ -3,7 +3,7 @@ package graphql import spock.lang.Specification class AssertTest extends Specification { - def "assertNull should not throw on none null value"() { + def "assertNotNull should not throw on none null value"() { when: Assert.assertNotNull("some object") @@ -11,7 +11,7 @@ class AssertTest extends Specification { noExceptionThrown() } - def "assertNull should throw on null value"() { + def "assertNotNull should throw on null value"() { when: Assert.assertNotNull(null) @@ -19,15 +19,24 @@ class AssertTest extends Specification { thrown(AssertException) } - def "assertNull with error message should not throw on none null value"() { + def "assertNotNull constant message should throw on null value"() { when: - Assert.assertNotNull("some object", { -> "error message"}) + Assert.assertNotNull(null, "constant message") + + then: + def error = thrown(AssertException) + error.message == "constant message" + } + + def "assertNotNull with error message should not throw on none null value"() { + when: + Assert.assertNotNull("some object", { -> "error message" }) then: noExceptionThrown() } - def "assertNull with error message should throw on null value with formatted message"() { + def "assertNotNull with error message should throw on null value with formatted message"() { when: Assert.assertNotNull(value, { -> String.format(format, arg) }) @@ -91,7 +100,7 @@ class AssertTest extends Specification { def "assertNotEmpty should not throw on none empty collection"() { when: - Assert.assertNotEmpty(["some object"], { -> "error message"}) + Assert.assertNotEmpty(["some object"], { -> "error message" }) then: noExceptionThrown() @@ -99,7 +108,7 @@ class AssertTest extends Specification { def "assertTrue should not throw on true value"() { when: - Assert.assertTrue(true, { ->"error message"}) + Assert.assertTrue(true, { -> "error message" }) then: noExceptionThrown() @@ -120,6 +129,47 @@ class AssertTest extends Specification { "code" | null || "code" } + def "assertTrue constant message should throw with message"() { + when: + Assert.assertTrue(false, "constant message") + + then: + def error = thrown(AssertException) + error.message == "constant message" + } + + def "assertFalse should throw"() { + when: + Assert.assertFalse(true) + + then: + thrown(AssertException) + } + + def "assertFalse constant message should throw with message"() { + when: + Assert.assertFalse(true, "constant message") + + then: + def error = thrown(AssertException) + error.message == "constant message" + } + + def "assertFalse with error message should throw on false value with formatted message"() { + when: + Assert.assertFalse(true, { -> String.format(format, arg) }) + + then: + def error = thrown(AssertException) + error.message == expectedMessage + + where: + format | arg || expectedMessage + "error %s" | "msg" || "error msg" + "code %d" | 1 || "code 1" + "code" | null || "code" + } + def "assertValidName should not throw on valid names"() { when: Assert.assertValidName(name) From b0a362e494d3bf3e76c950a21ca713a6b8b52b56 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 7 Mar 2024 21:24:49 +1100 Subject: [PATCH 295/393] Cheaper assertions with N arity functions --- src/main/java/graphql/Assert.java | 131 ++++++++++++------ .../analysis/NodeVisitorWithTypeTracking.java | 5 +- src/main/java/graphql/execution/Async.java | 6 +- .../execution/ValuesResolverLegacy.java | 2 +- .../ValuesResolverOneOfValidation.java | 4 +- .../conditional/ConditionalNodes.java | 2 +- .../incremental/IncrementalUtils.java | 7 +- .../persisted/PersistedQuerySupport.java | 2 +- .../graphql/introspection/Introspection.java | 4 +- .../java/graphql/language/AstPrinter.java | 3 +- .../normalized/ExecutableNormalizedField.java | 4 +- .../normalized/NormalizedInputValue.java | 4 +- .../graphql/schema/CodeRegistryVisitor.java | 4 +- .../graphql/schema/GraphQLCodeRegistry.java | 4 +- .../java/graphql/schema/GraphQLNonNull.java | 4 +- .../java/graphql/schema/GraphQLSchema.java | 6 +- .../schema/GraphQLTypeResolvingVisitor.java | 2 +- .../graphql/schema/SchemaTransformer.java | 4 +- .../graphql/schema/diffing/SchemaGraph.java | 16 +-- .../java/graphql/schema/diffing/Vertex.java | 2 +- .../idl/SchemaGeneratorDirectiveHelper.java | 2 +- .../schema/idl/SchemaGeneratorHelper.java | 2 +- src/main/java/graphql/util/Anonymizer.java | 11 +- .../java/graphql/util/TraverserState.java | 4 +- .../graphql/util/TreeParallelTransformer.java | 2 +- .../graphql/util/TreeParallelTraverser.java | 2 +- src/test/groovy/graphql/AssertTest.groovy | 93 +++++++++++++ src/test/java/benchmark/AssertBenchmark.java | 90 ++++++++++++ 28 files changed, 326 insertions(+), 96 deletions(-) create mode 100644 src/test/java/benchmark/AssertBenchmark.java diff --git a/src/main/java/graphql/Assert.java b/src/main/java/graphql/Assert.java index d780e419f3..872e19c05e 100644 --- a/src/main/java/graphql/Assert.java +++ b/src/main/java/graphql/Assert.java @@ -10,79 +10,92 @@ @Internal public class Assert { - public static T assertNotNull(T object, Supplier msg) { + public static T assertNotNullWithNPE(T object, Supplier msg) { if (object != null) { return object; } - throw new AssertException(msg.get()); + throw new NullPointerException(msg.get()); } - public static T assertNotNullWithNPE(T object, Supplier msg) { + public static T assertNotNull(T object) { if (object != null) { return object; } - throw new NullPointerException(msg.get()); + return throwAssert("Object required to be not null"); } - public static T assertNotNull(T object) { + public static T assertNotNull(T object, Supplier msg) { if (object != null) { return object; } - throw new AssertException("Object required to be not null"); + return throwAssert(msg.get()); } - /** - * If you use this method, make sure that the message is a constant string - * - * @param object the object to check for null - * @param constantMsg make sure this is a constant message - * @param for two - * - * @return the object - */ public static T assertNotNull(T object, String constantMsg) { if (object != null) { return object; } - throw new AssertException(constantMsg); + return throwAssert(constantMsg); + } + + public static T assertNotNull(T object, String msgFmt, Object arg1) { + if (object != null) { + return object; + } + return throwAssert(msgFmt, arg1); } + public static T assertNotNull(T object, String msgFmt, Object arg1, Object arg2) { + if (object != null) { + return object; + } + return throwAssert(msgFmt, arg1, arg2); + } + + public static T assertNotNull(T object, String msgFmt, Object arg1, Object arg2, Object arg3) { + if (object != null) { + return object; + } + return throwAssert(msgFmt, arg1, arg2, arg3); + } + + public static void assertNull(T object, Supplier msg) { if (object == null) { return; } - throw new AssertException(msg.get()); + throwAssert(msg.get()); } public static void assertNull(T object) { if (object == null) { return; } - throw new AssertException("Object required to be null"); + throwAssert("Object required to be null"); } public static T assertNeverCalled() { - throw new AssertException("Should never been called"); + return throwAssert("Should never been called"); } public static T assertShouldNeverHappen(String format, Object... args) { - throw new AssertException("Internal error: should never happen: " + format(format, args)); + return throwAssert("Internal error: should never happen: %s", format(format, args)); } public static T assertShouldNeverHappen() { - throw new AssertException("Internal error: should never happen"); + return throwAssert("Internal error: should never happen"); } public static Collection assertNotEmpty(Collection collection) { if (collection == null || collection.isEmpty()) { - throw new AssertException("collection must be not null and not empty"); + throwAssert("collection must be not null and not empty"); } return collection; } public static Collection assertNotEmpty(Collection collection, Supplier msg) { if (collection == null || collection.isEmpty()) { - throw new AssertException(msg.get()); + throwAssert(msg.get()); } return collection; } @@ -91,54 +104,84 @@ public static void assertTrue(boolean condition, Supplier msg) { if (condition) { return; } - throw new AssertException(msg.get()); + throwAssert(msg.get()); } public static void assertTrue(boolean condition) { if (condition) { return; } - throw new AssertException("condition expected to be true"); + throwAssert("condition expected to be true"); } - /** - * If you use this method, make sure that the message is a constant string - * - * @param condition the condition that must be true - * @param constantMsg make sure this is a constant message - */ public static void assertTrue(boolean condition, String constantMsg) { if (condition) { return; } - throw new AssertException(constantMsg); + throwAssert(constantMsg); + } + + public static void assertTrue(boolean condition, String msgFmt, Object arg1) { + if (condition) { + return; + } + throwAssert(msgFmt, arg1); + } + + public static void assertTrue(boolean condition, String msgFmt, Object arg1, Object arg2) { + if (condition) { + return; + } + throwAssert(msgFmt, arg1, arg2); + } + + public static void assertTrue(boolean condition, String msgFmt, Object arg1, Object arg2, Object arg3) { + if (condition) { + return; + } + throwAssert(msgFmt, arg1, arg2, arg3); } public static void assertFalse(boolean condition, Supplier msg) { if (!condition) { return; } - throw new AssertException(msg.get()); + throwAssert(msg.get()); } public static void assertFalse(boolean condition) { if (!condition) { return; } - throw new AssertException("condition expected to be false"); + throwAssert("condition expected to be false"); } - /** - * If you use this method, make sure that the message is a constant string - * - * @param condition the condition that must be false - * @param constantMsg make sure this is a constant message - */ public static void assertFalse(boolean condition, String constantMsg) { if (!condition) { return; } - throw new AssertException(constantMsg); + throwAssert(constantMsg); + } + + public static void assertFalse(boolean condition, String msgFmt, Object arg1) { + if (!condition) { + return; + } + throwAssert(msgFmt, arg1); + } + + public static void assertFalse(boolean condition, String msgFmt, Object arg1, Object arg2) { + if (!condition) { + return; + } + throwAssert(msgFmt, arg1, arg2); + } + + public static void assertFalse(boolean condition, String msgFmt, Object arg1, Object arg2, Object arg3) { + if (!condition) { + return; + } + throwAssert(msgFmt, arg1, arg2, arg3); } private static final String invalidNameErrorMessage = "Name must be non-null, non-empty and match [_A-Za-z][_0-9A-Za-z]* - was '%s'"; @@ -150,13 +193,17 @@ public static void assertFalse(boolean condition, String constantMsg) { * currently non null, non empty, * * @param name - the name to be validated. + * * @return the name if valid, or AssertException if invalid. */ public static String assertValidName(String name) { if (name != null && !name.isEmpty() && validNamePattern.matcher(name).matches()) { return name; } - throw new AssertException(String.format(invalidNameErrorMessage, name)); + return throwAssert(invalidNameErrorMessage, name); } + private static T throwAssert(String format, Object... args) { + throw new AssertException(format(format, args)); + } } diff --git a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java index 972a6f8e9c..683138e387 100644 --- a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java +++ b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java @@ -36,7 +36,6 @@ import static graphql.Assert.assertNotNull; import static graphql.schema.GraphQLTypeUtil.unwrapAll; import static graphql.util.TraverserContext.Phase.LEAVE; -import static java.lang.String.format; /** * Internally used node visitor which delegates to a {@link QueryVisitor} with type @@ -142,8 +141,8 @@ public TraversalControl visitFragmentSpread(FragmentSpread fragmentSpread, Trave GraphQLCompositeType typeCondition = (GraphQLCompositeType) schema.getType(fragmentDefinition.getTypeCondition().getName()); assertNotNull(typeCondition, - () -> format("Invalid type condition '%s' in fragment '%s'", fragmentDefinition.getTypeCondition().getName(), - fragmentDefinition.getName())); + "Invalid type condition '%s' in fragment '%s'", fragmentDefinition.getTypeCondition().getName(), + fragmentDefinition.getName()); context.setVar(QueryTraversalContext.class, new QueryTraversalContext(typeCondition, parentEnv.getEnvironment(), fragmentDefinition, graphQLContext)); return TraversalControl.CONTINUE; } diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index cbdd45d96b..f3eebc9e3e 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -86,7 +86,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == 1, () -> "expected size was " + 1 + " got " + ix); + Assert.assertTrue(ix == 1, "expected size was 1 got %d", ix); return completableFuture.thenApply(Collections::singletonList); } } @@ -109,7 +109,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == array.length, "expected size was less than the array length"); + Assert.assertTrue(ix == array.length, "expected size was %d got %d", array.length, ix); CompletableFuture> overallResult = new CompletableFuture<>(); CompletableFuture.allOf(array) @@ -160,7 +160,7 @@ private static void eachSequentiallyImpl(Iterator iterator, BiFunction CompletableFuture cf; try { cf = cfFactory.apply(iterator.next(), tmpResult); - Assert.assertNotNull(cf,"cfFactory must return a non null value"); + Assert.assertNotNull(cf, "cfFactory must return a non null value"); } catch (Exception e) { cf = new CompletableFuture<>(); cf.completeExceptionally(new CompletionException(e)); diff --git a/src/main/java/graphql/execution/ValuesResolverLegacy.java b/src/main/java/graphql/execution/ValuesResolverLegacy.java index 81bc80ccdc..d5e58f4656 100644 --- a/src/main/java/graphql/execution/ValuesResolverLegacy.java +++ b/src/main/java/graphql/execution/ValuesResolverLegacy.java @@ -49,7 +49,7 @@ class ValuesResolverLegacy { */ @VisibleForTesting static Value valueToLiteralLegacy(Object value, GraphQLType type, GraphQLContext graphqlContext, Locale locale) { - assertTrue(!(value instanceof Value), () -> "Unexpected literal " + value); + assertTrue(!(value instanceof Value), "Unexpected literal %s", value); if (value == null) { return null; } diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index c0f98f5523..9955ce050d 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -42,7 +42,7 @@ static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value String.format("The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName)); + Assert.assertTrue(inputValue instanceof Map, "The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName); Map objectMap = (Map) inputValue; GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedNonNullType; @@ -63,7 +63,7 @@ static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value 1) { - Assert.assertShouldNeverHappen(String.format("argument %s has %s object fields with the same name: '%s'. A maximum of 1 is expected", argumentName, values.size(), childFieldName)); + Assert.assertShouldNeverHappen("argument %s has %s object fields with the same name: '%s'. A maximum of 1 is expected", argumentName, values.size(), childFieldName); } else if (!values.isEmpty()) { validateOneOfInputTypes(childFieldType, childFieldInputValue, values.get(0), argumentName, locale); } diff --git a/src/main/java/graphql/execution/conditional/ConditionalNodes.java b/src/main/java/graphql/execution/conditional/ConditionalNodes.java index 9c90deead0..7013c53bf3 100644 --- a/src/main/java/graphql/execution/conditional/ConditionalNodes.java +++ b/src/main/java/graphql/execution/conditional/ConditionalNodes.java @@ -93,7 +93,7 @@ private boolean getDirectiveResult(Map variables, List argumentValues = ValuesResolver.getArgumentValues(SkipDirective.getArguments(), foundDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); Object flag = argumentValues.get("if"); - Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", directiveName)); + Assert.assertTrue(flag instanceof Boolean, "The '%s' directive MUST have a value for the 'if' argument", directiveName); return (Boolean) flag; } return defaultValue; diff --git a/src/main/java/graphql/execution/incremental/IncrementalUtils.java b/src/main/java/graphql/execution/incremental/IncrementalUtils.java index 78d4655027..2a89ade3fd 100644 --- a/src/main/java/graphql/execution/incremental/IncrementalUtils.java +++ b/src/main/java/graphql/execution/incremental/IncrementalUtils.java @@ -17,7 +17,8 @@ @Internal public class IncrementalUtils { - private IncrementalUtils() {} + private IncrementalUtils() { + } public static T createDeferredExecution( Map variables, @@ -30,7 +31,7 @@ public static T createDeferredExecution( Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); Object flag = argumentValues.get("if"); - Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + Assert.assertTrue(flag instanceof Boolean, "The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName()); if (!((Boolean) flag)) { return null; @@ -42,7 +43,7 @@ public static T createDeferredExecution( return builderFunction.apply(null); } - Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); + Assert.assertTrue(label instanceof String, "The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName()); return builderFunction.apply((String) label); } diff --git a/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java b/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java index af2edef379..dc5357b448 100644 --- a/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java +++ b/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java @@ -38,7 +38,7 @@ public PersistedQuerySupport(PersistedQueryCache persistedQueryCache) { @Override public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction) { Optional queryIdOption = getPersistedQueryId(executionInput); - assertNotNull(queryIdOption, () -> String.format("The class %s MUST return a non null optional query id", this.getClass().getName())); + assertNotNull(queryIdOption, "The class %s MUST return a non null optional query id", this.getClass().getName()); try { if (queryIdOption.isPresent()) { diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index d496e03702..d1a6d97124 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -714,10 +714,10 @@ public static GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLCo return schema.getIntrospectionTypenameFieldDefinition(); } - assertTrue(parentType instanceof GraphQLFieldsContainer, () -> String.format("should not happen : parent type must be an object or interface %s", parentType)); + assertTrue(parentType instanceof GraphQLFieldsContainer, "Should not happen : parent type must be an object or interface %s", parentType); GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) parentType; GraphQLFieldDefinition fieldDefinition = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, fieldName); - assertTrue(fieldDefinition != null, () -> String.format("Unknown field '%s' for type %s", fieldName, fieldsContainer.getName())); + assertTrue(fieldDefinition != null, "Unknown field '%s' for type %s", fieldName, fieldsContainer.getName()); return fieldDefinition; } } diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index df2c1f71ea..e032c52c3b 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.StringJoiner; +import static graphql.Assert.assertShouldNeverHappen; import static graphql.Assert.assertTrue; import static graphql.util.EscapeUtil.escapeJsonString; import static java.lang.String.valueOf; @@ -483,7 +484,7 @@ NodePrinter _findPrinter(Node node, Class startClass) { } clazz = clazz.getSuperclass(); } - throw new AssertException(String.format("We have a missing printer implementation for %s : report a bug!", clazz)); + return assertShouldNeverHappen("We have a missing printer implementation for %s : report a bug!", clazz); } private boolean isEmpty(List list) { diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index ebc5a21727..01178b8160 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -201,7 +201,7 @@ public void forEachFieldDefinition(GraphQLSchema schema, Consumer String.format("No field %s found for type %s", fieldName, objectTypeName))); + consumer.accept(assertNotNull(type.getField(fieldName), "No field %s found for type %s", fieldName, objectTypeName)); } } @@ -224,7 +224,7 @@ private GraphQLFieldDefinition getOneFieldDefinition(GraphQLSchema schema) { String objectTypeName = objectTypeNames.iterator().next(); GraphQLObjectType type = (GraphQLObjectType) assertNotNull(schema.getType(objectTypeName)); - return assertNotNull(type.getField(fieldName), () -> String.format("No field %s found for type %s", fieldName, objectTypeName)); + return assertNotNull(type.getField(fieldName), "No field %s found for type %s", fieldName, objectTypeName); } private static GraphQLFieldDefinition resolveIntrospectionField(GraphQLSchema schema, Set objectTypeNames, String fieldName) { diff --git a/src/main/java/graphql/normalized/NormalizedInputValue.java b/src/main/java/graphql/normalized/NormalizedInputValue.java index a73d90c4a9..181e8594b1 100644 --- a/src/main/java/graphql/normalized/NormalizedInputValue.java +++ b/src/main/java/graphql/normalized/NormalizedInputValue.java @@ -107,8 +107,8 @@ private String unwrapOne(String typeName) { } if (isListOnly(typeName)) { // nominally this will never be true - but better to be safe than sorry - assertTrue(typeName.startsWith("["), () -> String.format("We have a unbalanced list type string '%s'", typeName)); - assertTrue(typeName.endsWith("]"), () -> String.format("We have a unbalanced list type string '%s'", typeName)); + assertTrue(typeName.startsWith("["), "We have a unbalanced list type string '%s'", typeName); + assertTrue(typeName.endsWith("]"), "We have a unbalanced list type string '%s'", typeName); return typeName.substring(1, typeName.length() - 1); } diff --git a/src/main/java/graphql/schema/CodeRegistryVisitor.java b/src/main/java/graphql/schema/CodeRegistryVisitor.java index 50cfa2a492..166ed6239c 100644 --- a/src/main/java/graphql/schema/CodeRegistryVisitor.java +++ b/src/main/java/graphql/schema/CodeRegistryVisitor.java @@ -40,7 +40,7 @@ public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType node, Tra codeRegistry.typeResolverIfAbsent(node, typeResolver); } assertTrue(codeRegistry.getTypeResolver(node) != null, - () -> String.format("You MUST provide a type resolver for the interface type '%s'", node.getName())); + "You MUST provide a type resolver for the interface type '%s'", node.getName()); return CONTINUE; } @@ -51,7 +51,7 @@ public TraversalControl visitGraphQLUnionType(GraphQLUnionType node, TraverserCo codeRegistry.typeResolverIfAbsent(node, typeResolver); } assertTrue(codeRegistry.getTypeResolver(node) != null, - () -> String.format("You MUST provide a type resolver for the union type '%s'", node.getName())); + "You MUST provide a type resolver for the union type '%s'", node.getName()); return CONTINUE; } } diff --git a/src/main/java/graphql/schema/GraphQLCodeRegistry.java b/src/main/java/graphql/schema/GraphQLCodeRegistry.java index d1b0f94d4c..e72fadf080 100644 --- a/src/main/java/graphql/schema/GraphQLCodeRegistry.java +++ b/src/main/java/graphql/schema/GraphQLCodeRegistry.java @@ -157,7 +157,7 @@ private static TypeResolver getTypeResolverForInterface(GraphQLInterfaceType par if (typeResolver == null) { typeResolver = parentType.getTypeResolver(); } - return assertNotNull(typeResolver, () -> "There must be a type resolver for interface " + parentType.getName()); + return assertNotNull(typeResolver, "There must be a type resolver for interface %s", parentType.getName()); } private static TypeResolver getTypeResolverForUnion(GraphQLUnionType parentType, Map typeResolverMap) { @@ -166,7 +166,7 @@ private static TypeResolver getTypeResolverForUnion(GraphQLUnionType parentType, if (typeResolver == null) { typeResolver = parentType.getTypeResolver(); } - return assertNotNull(typeResolver, () -> "There must be a type resolver for union " + parentType.getName()); + return assertNotNull(typeResolver, "There must be a type resolver for union %s",parentType.getName()); } /** diff --git a/src/main/java/graphql/schema/GraphQLNonNull.java b/src/main/java/graphql/schema/GraphQLNonNull.java index 6de1ba61d3..12f131b428 100644 --- a/src/main/java/graphql/schema/GraphQLNonNull.java +++ b/src/main/java/graphql/schema/GraphQLNonNull.java @@ -46,8 +46,8 @@ public GraphQLNonNull(GraphQLType wrappedType) { } private void assertNonNullWrapping(GraphQLType wrappedType) { - assertTrue(!GraphQLTypeUtil.isNonNull(wrappedType), () -> - String.format("A non null type cannot wrap an existing non null type '%s'", GraphQLTypeUtil.simplePrint(wrappedType))); + assertTrue(!GraphQLTypeUtil.isNonNull(wrappedType), + "A non null type cannot wrap an existing non null type '%s'", GraphQLTypeUtil.simplePrint(wrappedType)); } @Override diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index e7026ad6c3..af95dd7258 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -248,7 +248,7 @@ public Set getAdditionalTypes() { public List getTypes(Collection typeNames) { ImmutableList.Builder builder = ImmutableList.builder(); for (String typeName : typeNames) { - builder.add((T) assertNotNull(typeMap.get(typeName), () -> String.format("No type found for name %s", typeName))); + builder.add((T) assertNotNull(typeMap.get(typeName), "No type found for name %s", typeName)); } return builder.build(); } @@ -292,7 +292,7 @@ public GraphQLObjectType getObjectType(String typeName) { GraphQLType graphQLType = typeMap.get(typeName); if (graphQLType != null) { assertTrue(graphQLType instanceof GraphQLObjectType, - () -> String.format("You have asked for named object type '%s' but it's not an object type but rather a '%s'", typeName, graphQLType.getClass().getName())); + "You have asked for named object type '%s' but it's not an object type but rather a '%s'", typeName, graphQLType.getClass().getName()); } return (GraphQLObjectType) graphQLType; } @@ -323,7 +323,7 @@ public GraphQLFieldDefinition getFieldDefinition(FieldCoordinates fieldCoordinat GraphQLType graphQLType = getType(typeName); if (graphQLType != null) { assertTrue(graphQLType instanceof GraphQLFieldsContainer, - () -> String.format("You have asked for named type '%s' but it's not GraphQLFieldsContainer but rather a '%s'", typeName, graphQLType.getClass().getName())); + "You have asked for named type '%s' but it's not GraphQLFieldsContainer but rather a '%s'", typeName, graphQLType.getClass().getName()); return ((GraphQLFieldsContainer) graphQLType).getFieldDefinition(fieldName); } return null; diff --git a/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java b/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java index 0380ac5cd1..37a57f8933 100644 --- a/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java +++ b/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java @@ -46,7 +46,7 @@ public TraversalControl visitGraphQLTypeReference(GraphQLTypeReference node, Tra public TraversalControl handleTypeReference(GraphQLTypeReference node, TraverserContext context) { final GraphQLType resolvedType = typeMap.get(node.getName()); - assertNotNull(resolvedType, () -> String.format("type %s not found in schema", node.getName())); + assertNotNull(resolvedType, "type %s not found in schema", node.getName()); context.getParentContext().thisNode().accept(context, new TypeRefResolvingVisitor(resolvedType)); return CONTINUE; } diff --git a/src/main/java/graphql/schema/SchemaTransformer.java b/src/main/java/graphql/schema/SchemaTransformer.java index 06b6cfe08b..31846b6f85 100644 --- a/src/main/java/graphql/schema/SchemaTransformer.java +++ b/src/main/java/graphql/schema/SchemaTransformer.java @@ -322,7 +322,7 @@ public void updateZipper(NodeZipper currentZipper, List>> currentBreadcrumbs = breadcrumbsByZipper.get(currentZipper); - assertNotNull(currentBreadcrumbs, () -> format("No breadcrumbs found for zipper %s", currentZipper)); + assertNotNull(currentBreadcrumbs, "No breadcrumbs found for zipper %s", currentZipper); for (List> breadcrumbs : currentBreadcrumbs) { GraphQLSchemaElement parent = breadcrumbs.get(0).getNode(); zipperByParent.remove(parent, currentZipper); @@ -392,7 +392,7 @@ private boolean zipUpToDummyRoot(List> zippers, } NodeZipper curZipperForElement = nodeToZipper.get(element); - assertNotNull(curZipperForElement, () -> format("curZipperForElement is null for parentNode %s", element)); + assertNotNull(curZipperForElement, "curZipperForElement is null for parentNode %s", element); relevantZippers.updateZipper(curZipperForElement, newZipper); } diff --git a/src/main/java/graphql/schema/diffing/SchemaGraph.java b/src/main/java/graphql/schema/diffing/SchemaGraph.java index edaebe5284..c12c109fb7 100644 --- a/src/main/java/graphql/schema/diffing/SchemaGraph.java +++ b/src/main/java/graphql/schema/diffing/SchemaGraph.java @@ -230,31 +230,31 @@ public List addIsolatedVertices(int count, String debugPrefix) { public Vertex getFieldOrDirectiveForArgument(Vertex argument) { List adjacentVertices = getAdjacentVerticesInverse(argument); - assertTrue(adjacentVertices.size() == 1, () -> format("No field or directive found for %s", argument)); + assertTrue(adjacentVertices.size() == 1, "No field or directive found for %s", argument); return adjacentVertices.get(0); } public Vertex getFieldsContainerForField(Vertex field) { List adjacentVertices = getAdjacentVerticesInverse(field); - assertTrue(adjacentVertices.size() == 1, () -> format("No fields container found for %s", field)); + assertTrue(adjacentVertices.size() == 1, "No fields container found for %s", field); return adjacentVertices.get(0); } public Vertex getInputObjectForInputField(Vertex inputField) { List adjacentVertices = this.getAdjacentVerticesInverse(inputField); - assertTrue(adjacentVertices.size() == 1, () -> format("No input object found for %s", inputField)); + assertTrue(adjacentVertices.size() == 1, "No input object found for %s", inputField); return adjacentVertices.get(0); } public Vertex getAppliedDirectiveForAppliedArgument(Vertex appliedArgument) { List adjacentVertices = this.getAdjacentVerticesInverse(appliedArgument); - assertTrue(adjacentVertices.size() == 1, () -> format("No applied directive found for %s", appliedArgument)); + assertTrue(adjacentVertices.size() == 1, "No applied directive found for %s", appliedArgument); return adjacentVertices.get(0); } public Vertex getAppliedDirectiveContainerForAppliedDirective(Vertex appliedDirective) { List adjacentVertices = this.getAdjacentVerticesInverse(appliedDirective); - assertTrue(adjacentVertices.size() == 1, () -> format("No applied directive container found for %s", appliedDirective)); + assertTrue(adjacentVertices.size() == 1, "No applied directive container found for %s", appliedDirective); return adjacentVertices.get(0); } @@ -266,19 +266,19 @@ public Vertex getAppliedDirectiveContainerForAppliedDirective(Vertex appliedDire */ public Vertex getSingleAdjacentInverseVertex(Vertex input) { Collection adjacentVertices = this.getAdjacentEdgesInverseNonCopy(input); - assertTrue(adjacentVertices.size() == 1, () -> format("No parent found for %s", input)); + assertTrue(adjacentVertices.size() == 1, "No parent found for %s", input); return adjacentVertices.iterator().next().getFrom(); } public int getAppliedDirectiveIndex(Vertex appliedDirective) { List adjacentEdges = this.getAdjacentEdgesInverseCopied(appliedDirective); - assertTrue(adjacentEdges.size() == 1, () -> format("No applied directive container found for %s", appliedDirective)); + assertTrue(adjacentEdges.size() == 1, "No applied directive container found for %s", appliedDirective); return Integer.parseInt(adjacentEdges.get(0).getLabel()); } public Vertex getEnumForEnumValue(Vertex enumValue) { List adjacentVertices = this.getAdjacentVerticesInverse(enumValue); - assertTrue(adjacentVertices.size() == 1, () -> format("No enum found for %s", enumValue)); + assertTrue(adjacentVertices.size() == 1, "No enum found for %s", enumValue); return adjacentVertices.get(0); } diff --git a/src/main/java/graphql/schema/diffing/Vertex.java b/src/main/java/graphql/schema/diffing/Vertex.java index 3a39a34e62..4b408a37c1 100644 --- a/src/main/java/graphql/schema/diffing/Vertex.java +++ b/src/main/java/graphql/schema/diffing/Vertex.java @@ -62,7 +62,7 @@ public T getProperty(String name) { } public String getName() { - return (String) Assert.assertNotNull(properties.get("name"), () -> String.format("should not call getName on %s", this)); + return (String) Assert.assertNotNull(properties.get("name"), "should not call getName on %s", this); } public Map getProperties() { diff --git a/src/main/java/graphql/schema/idl/SchemaGeneratorDirectiveHelper.java b/src/main/java/graphql/schema/idl/SchemaGeneratorDirectiveHelper.java index 8de58a983b..824ee17f41 100644 --- a/src/main/java/graphql/schema/idl/SchemaGeneratorDirectiveHelper.java +++ b/src/main/java/graphql/schema/idl/SchemaGeneratorDirectiveHelper.java @@ -464,7 +464,7 @@ private T wireDirectives( private T invokeWiring(T element, EnvInvoker invoker, SchemaDirectiveWiring schemaDirectiveWiring, SchemaDirectiveWiringEnvironment env) { T newElement = invoker.apply(schemaDirectiveWiring, env); - assertNotNull(newElement, () -> "The SchemaDirectiveWiring MUST return a non null return value for element '" + element.getName() + "'"); + assertNotNull(newElement, "The SchemaDirectiveWiring MUST return a non null return value for element '%s'",element.getName()); return newElement; } diff --git a/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java b/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java index 317828aaf8..aa4878efb4 100644 --- a/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java +++ b/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java @@ -401,7 +401,7 @@ private GraphQLEnumValueDefinition buildEnumValue(BuildContext buildCtx, if (enumValuesProvider != null) { value = enumValuesProvider.getValue(evd.getName()); assertNotNull(value, - () -> format("EnumValuesProvider for %s returned null for %s", typeDefinition.getName(), evd.getName())); + "EnumValuesProvider for %s returned null for %s", typeDefinition.getName(), evd.getName()); } else { value = evd.getName(); } diff --git a/src/main/java/graphql/util/Anonymizer.java b/src/main/java/graphql/util/Anonymizer.java index 34553d8777..2d0497eca6 100644 --- a/src/main/java/graphql/util/Anonymizer.java +++ b/src/main/java/graphql/util/Anonymizer.java @@ -100,7 +100,6 @@ import static graphql.schema.idl.SchemaGenerator.createdMockedSchema; import static graphql.util.TraversalControl.CONTINUE; import static graphql.util.TreeTransformerUtil.changeNode; -import static java.lang.String.format; /** * Util class which converts schemas and optionally queries @@ -735,14 +734,14 @@ public void visitField(QueryVisitorFieldEnvironment env) { List directives = field.getDirectives(); for (Directive directive : directives) { // this is a directive definition - GraphQLDirective directiveDefinition = assertNotNull(schema.getDirective(directive.getName()), () -> format("%s directive definition not found ", directive.getName())); + GraphQLDirective directiveDefinition = assertNotNull(schema.getDirective(directive.getName()), "%s directive definition not found ", directive.getName()); String directiveName = directiveDefinition.getName(); - String newDirectiveName = assertNotNull(newNames.get(directiveDefinition), () -> format("No new name found for directive %s", directiveName)); + String newDirectiveName = assertNotNull(newNames.get(directiveDefinition), "No new name found for directive %s", directiveName); astNodeToNewName.put(directive, newDirectiveName); for (Argument argument : directive.getArguments()) { GraphQLArgument argumentDefinition = directiveDefinition.getArgument(argument.getName()); - String newArgumentName = assertNotNull(newNames.get(argumentDefinition), () -> format("No new name found for directive %s argument %s", directiveName, argument.getName())); + String newArgumentName = assertNotNull(newNames.get(argumentDefinition), "No new name found for directive %s argument %s", directiveName, argument.getName()); astNodeToNewName.put(argument, newArgumentName); visitDirectiveArgumentValues(directive, argument.getValue()); } @@ -865,7 +864,7 @@ public TraversalControl visitVariableDefinition(VariableDefinition node, Travers @Override public TraversalControl visitVariableReference(VariableReference node, TraverserContext context) { - String newName = assertNotNull(variableNames.get(node.getName()), () -> format("No new variable name found for %s", node.getName())); + String newName = assertNotNull(variableNames.get(node.getName()), "No new variable name found for %s", node.getName()); return changeNode(context, node.transform(builder -> builder.name(newName))); } @@ -916,7 +915,7 @@ private static GraphQLType fromTypeToGraphQLType(Type type, GraphQLSchema schema if (type instanceof TypeName) { String typeName = ((TypeName) type).getName(); GraphQLType graphQLType = schema.getType(typeName); - graphql.Assert.assertNotNull(graphQLType, () -> "Schema must contain type " + typeName); + graphql.Assert.assertNotNull(graphQLType, "Schema must contain type %s", typeName); return graphQLType; } else if (type instanceof NonNullType) { return GraphQLNonNull.nonNull(fromTypeToGraphQLType(TypeUtil.unwrapOne(type), schema)); diff --git a/src/main/java/graphql/util/TraverserState.java b/src/main/java/graphql/util/TraverserState.java index e44058f914..e478e05a0b 100644 --- a/src/main/java/graphql/util/TraverserState.java +++ b/src/main/java/graphql/util/TraverserState.java @@ -44,7 +44,7 @@ public void pushAll(TraverserContext traverserContext, Function { List children = childrenMap.get(key); for (int i = children.size() - 1; i >= 0; i--) { - U child = assertNotNull(children.get(i), () -> "null child for key " + key); + U child = assertNotNull(children.get(i), "null child for key %s",key); NodeLocation nodeLocation = new NodeLocation(key, i); DefaultTraverserContext context = super.newContext(child, traverserContext, nodeLocation); super.state.push(context); @@ -72,7 +72,7 @@ public void pushAll(TraverserContext traverserContext, Function { List children = childrenMap.get(key); for (int i = 0; i < children.size(); i++) { - U child = assertNotNull(children.get(i), () -> "null child for key " + key); + U child = assertNotNull(children.get(i), "null child for key %s",key); NodeLocation nodeLocation = new NodeLocation(key, i); DefaultTraverserContext context = super.newContext(child, traverserContext, nodeLocation); childrenContextMap.computeIfAbsent(key, notUsed -> new ArrayList<>()); diff --git a/src/main/java/graphql/util/TreeParallelTransformer.java b/src/main/java/graphql/util/TreeParallelTransformer.java index 0102af2eff..3003ac3772 100644 --- a/src/main/java/graphql/util/TreeParallelTransformer.java +++ b/src/main/java/graphql/util/TreeParallelTransformer.java @@ -223,7 +223,7 @@ private List pushAll(TraverserContext traverserConte childrenMap.keySet().forEach(key -> { List children = childrenMap.get(key); for (int i = children.size() - 1; i >= 0; i--) { - T child = assertNotNull(children.get(i), () -> String.format("null child for key %s", key)); + T child = assertNotNull(children.get(i), "null child for key %s", key); NodeLocation nodeLocation = new NodeLocation(key, i); DefaultTraverserContext context = newContext(child, traverserContext, nodeLocation); contexts.push(context); diff --git a/src/main/java/graphql/util/TreeParallelTraverser.java b/src/main/java/graphql/util/TreeParallelTraverser.java index db7f27dbaa..75a903be13 100644 --- a/src/main/java/graphql/util/TreeParallelTraverser.java +++ b/src/main/java/graphql/util/TreeParallelTraverser.java @@ -162,7 +162,7 @@ private List pushAll(TraverserContext traverserConte childrenMap.keySet().forEach(key -> { List children = childrenMap.get(key); for (int i = children.size() - 1; i >= 0; i--) { - T child = assertNotNull(children.get(i), () -> String.format("null child for key %s", key)); + T child = assertNotNull(children.get(i), "null child for key %s", key); NodeLocation nodeLocation = new NodeLocation(key, i); DefaultTraverserContext context = newContext(child, traverserContext, nodeLocation); contexts.push(context); diff --git a/src/test/groovy/graphql/AssertTest.groovy b/src/test/groovy/graphql/AssertTest.groovy index 13a4bae364..5d6901ba23 100644 --- a/src/test/groovy/graphql/AssertTest.groovy +++ b/src/test/groovy/graphql/AssertTest.groovy @@ -51,6 +51,35 @@ class AssertTest extends Specification { null | "code" | null || "code" } + def "assertNotNull with different number of error args throws assertions"() { + when: + toRun.run() + + then: + def error = thrown(AssertException) + error.message == expectedMessage + + where: + toRun | expectedMessage + runnable({ Assert.assertNotNull(null, "error %s", "arg1") }) | "error arg1" + runnable({ Assert.assertNotNull(null, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ Assert.assertNotNull(null, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + } + + def "assertNotNull with different number of error args with non null does not throw assertions"() { + when: + toRun.run() + + then: + noExceptionThrown() + + where: + toRun | expectedMessage + runnable({ Assert.assertNotNull("x", "error %s", "arg1") }) | "error arg1" + runnable({ Assert.assertNotNull("x", "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ Assert.assertNotNull("x", "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + } + def "assertNeverCalled should always throw"() { when: Assert.assertNeverCalled() @@ -138,6 +167,35 @@ class AssertTest extends Specification { error.message == "constant message" } + def "assertTrue with different number of error args throws assertions"() { + when: + toRun.run() + + then: + def error = thrown(AssertException) + error.message == expectedMessage + + where: + toRun | expectedMessage + runnable({ Assert.assertTrue(false, "error %s", "arg1") }) | "error arg1" + runnable({ Assert.assertTrue(false, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ Assert.assertTrue(false, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + } + + def "assertTrue with different number of error args but false does not throw assertions"() { + when: + toRun.run() + + then: + noExceptionThrown() + + where: + toRun | expectedMessage + runnable({ Assert.assertTrue(true, "error %s", "arg1") }) | "error arg1" + runnable({ Assert.assertTrue(true, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ Assert.assertTrue(true, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + } + def "assertFalse should throw"() { when: Assert.assertFalse(true) @@ -170,6 +228,35 @@ class AssertTest extends Specification { "code" | null || "code" } + def "assertFalse with different number of error args throws assertions"() { + when: + toRun.run() + + then: + def error = thrown(AssertException) + error.message == expectedMessage + + where: + toRun | expectedMessage + runnable({ Assert.assertFalse(true, "error %s", "arg1") }) | "error arg1" + runnable({ Assert.assertFalse(true, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ Assert.assertFalse(true, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + } + + def "assertFalse with different number of error args but false does not throw assertions"() { + when: + toRun.run() + + then: + noExceptionThrown() + + where: + toRun | expectedMessage + runnable({ Assert.assertFalse(false, "error %s", "arg1") }) | "error arg1" + runnable({ Assert.assertFalse(false, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ Assert.assertFalse(false, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + } + def "assertValidName should not throw on valid names"() { when: Assert.assertValidName(name) @@ -200,4 +287,10 @@ class AssertTest extends Specification { "���" | _ "_()" | _ } + + // Spock data tables cant cope with { x } syntax but it cna do this + Runnable runnable(Runnable r) { + return r + } + } diff --git a/src/test/java/benchmark/AssertBenchmark.java b/src/test/java/benchmark/AssertBenchmark.java new file mode 100644 index 0000000000..192b267280 --- /dev/null +++ b/src/test/java/benchmark/AssertBenchmark.java @@ -0,0 +1,90 @@ +package benchmark; + +import graphql.Assert; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Warmup(iterations = 2, time = 5, batchSize = 50) +@Measurement(iterations = 3, batchSize = 50) +@Fork(3) +public class AssertBenchmark { + + private static final int LOOPS = 100; + private static final Random random = new Random(); + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void benchMarkAssertWithString() { + for (int i = 0; i < LOOPS; i++) { + Assert.assertTrue(jitTrue(), "This string is constant"); + } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void benchMarkAssertWithStringSupplier() { + for (int i = 0; i < LOOPS; i++) { + Assert.assertTrue(jitTrue(), () -> "This string is constant"); + } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void benchMarkAssertWithStringSupplierFormatted() { + for (int i = 0; i < LOOPS; i++) { + final int captured = i; + Assert.assertTrue(jitTrue(), () -> String.format("This string is not constant %d", captured)); + } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void benchMarkAssertWithStringFormatted() { + for (int i = 0; i < LOOPS; i++) { + Assert.assertTrue(jitTrue(), "This string is not constant %d", i); + } + } + + private boolean jitTrue() { + int i = random.nextInt(); + // can you jit this away, Mr JIT?? + return i % 10 < -1 || i * 2 + 1 < -2; + } + + public static void main(String[] args) throws RunnerException { + runAtStartup(); + Options opt = new OptionsBuilder() + .include("benchmark.AssertBenchmark") + .build(); + + new Runner(opt).run(); + } + + private static void runAtStartup() { + AssertBenchmark benchMark = new AssertBenchmark(); + BenchmarkUtils.runInToolingForSomeTimeThenExit( + () -> { + }, + benchMark::benchMarkAssertWithStringSupplier, + () -> { + } + + ); + } +} From 34adae60b104e98295051c6842fcfed663641d01 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 7 Mar 2024 21:46:52 +1100 Subject: [PATCH 296/393] put () -> back --- src/main/java/graphql/GraphqlErrorBuilder.java | 2 +- src/main/java/graphql/execution/Async.java | 6 +++--- src/main/java/graphql/execution/ExecutionStepInfo.java | 6 ++---- .../graphql/execution/ExecutionStrategyParameters.java | 6 +++--- .../graphql/execution/FieldCollectorParameters.java | 2 +- src/main/java/graphql/execution/FieldValueInfo.java | 2 +- src/main/java/graphql/execution/ResultPath.java | 10 +++++----- .../execution/SubscriptionExecutionStrategy.java | 2 +- .../graphql/execution/ValuesResolverConversion.java | 2 +- .../execution/reactive/NonBlockingMutexExecutor.java | 2 +- .../java/graphql/extensions/ExtensionsBuilder.java | 2 +- src/main/java/graphql/language/AbstractNode.java | 6 +++--- src/main/java/graphql/language/AstPrinter.java | 2 +- src/main/java/graphql/language/NodeParentTree.java | 4 ++-- src/main/java/graphql/language/PrettyAstPrinter.java | 2 +- .../graphql/normalized/ExecutableNormalizedField.java | 4 ++-- .../java/graphql/normalized/NormalizedInputValue.java | 2 +- src/main/java/graphql/schema/AsyncDataFetcher.java | 4 ++-- .../schema/DefaultGraphqlTypeComparatorRegistry.java | 6 +++--- src/main/java/graphql/schema/GraphQLTypeUtil.java | 2 +- src/main/java/graphql/schema/InputValueWithState.java | 2 +- 21 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/main/java/graphql/GraphqlErrorBuilder.java b/src/main/java/graphql/GraphqlErrorBuilder.java index c7281a5f53..ae4ad5095f 100644 --- a/src/main/java/graphql/GraphqlErrorBuilder.java +++ b/src/main/java/graphql/GraphqlErrorBuilder.java @@ -129,7 +129,7 @@ public B extensions(@Nullable Map extensions) { * @return a newly built GraphqlError */ public GraphQLError build() { - Assert.assertNotNull(message,"You must provide error message"); + Assert.assertNotNull(message,() -> "You must provide error message"); return new GraphqlErrorImpl(message, locations, errorType, path, extensions); } diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index f3eebc9e3e..498acaba6f 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -58,7 +58,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == 0, "expected size was 0"); + Assert.assertTrue(ix == 0, () -> "expected size was 0"); return typedEmpty(); } @@ -135,7 +135,7 @@ public static CompletableFuture> each(Collection list, Functio CompletableFuture cf; try { cf = cfFactory.apply(t); - Assert.assertNotNull(cf, "cfFactory must return a non null value"); + Assert.assertNotNull(cf, () -> "cfFactory must return a non null value"); } catch (Exception e) { cf = new CompletableFuture<>(); // Async.each makes sure that it is not a CompletionException inside a CompletionException @@ -160,7 +160,7 @@ private static void eachSequentiallyImpl(Iterator iterator, BiFunction CompletableFuture cf; try { cf = cfFactory.apply(iterator.next(), tmpResult); - Assert.assertNotNull(cf, "cfFactory must return a non null value"); + Assert.assertNotNull(cf, () -> "cfFactory must return a non null value"); } catch (Exception e) { cf = new CompletableFuture<>(); cf.completeExceptionally(new CompletionException(e)); diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index 42e0b01ebb..c7074a5c7f 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfo.java +++ b/src/main/java/graphql/execution/ExecutionStepInfo.java @@ -14,8 +14,6 @@ import java.util.function.Consumer; import java.util.function.Supplier; -import static graphql.Assert.assertNotNull; -import static graphql.Assert.assertTrue; import static graphql.schema.GraphQLTypeUtil.isList; /** @@ -73,7 +71,7 @@ private ExecutionStepInfo(Builder builder) { this.field = builder.field; this.path = builder.path; this.parent = builder.parentInfo; - this.type = Assert.assertNotNull(builder.type, "you must provide a graphql type"); + this.type = Assert.assertNotNull(builder.type, () -> "you must provide a graphql type"); this.arguments = builder.arguments; this.fieldContainer = builder.fieldContainer; } @@ -203,7 +201,7 @@ public boolean hasParent() { * @return a new type info with the same */ public ExecutionStepInfo changeTypeWithPreservedNonNull(GraphQLOutputType newType) { - Assert.assertTrue(!GraphQLTypeUtil.isNonNull(newType), "newType can't be non null"); + Assert.assertTrue(!GraphQLTypeUtil.isNonNull(newType), () -> "newType can't be non null"); if (isNonNullType()) { return newExecutionStepInfo(this).type(GraphQLNonNull.nonNull(newType)).build(); } else { diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index 4df2b73605..915a4cfab6 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -33,9 +33,9 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, ExecutionStrategyParameters parent, DeferredCallContext deferredCallContext) { - this.executionStepInfo = Assert.assertNotNull(executionStepInfo, "executionStepInfo is null"); + this.executionStepInfo = Assert.assertNotNull(executionStepInfo, () -> "executionStepInfo is null"); this.localContext = localContext; - this.fields = Assert.assertNotNull(fields, "fields is null"); + this.fields = Assert.assertNotNull(fields, () -> "fields is null"); this.source = source; this.nonNullableFieldValidator = nonNullableFieldValidator; this.path = path; @@ -168,7 +168,7 @@ public Builder localContext(Object localContext) { } public Builder nonNullFieldValidator(NonNullableFieldValidator nonNullableFieldValidator) { - this.nonNullableFieldValidator = Assert.assertNotNull(nonNullableFieldValidator, "requires a NonNullValidator"); + this.nonNullableFieldValidator = Assert.assertNotNull(nonNullableFieldValidator, () -> "requires a NonNullValidator"); return this; } diff --git a/src/main/java/graphql/execution/FieldCollectorParameters.java b/src/main/java/graphql/execution/FieldCollectorParameters.java index 75dafc1eff..c0a23404a7 100644 --- a/src/main/java/graphql/execution/FieldCollectorParameters.java +++ b/src/main/java/graphql/execution/FieldCollectorParameters.java @@ -92,7 +92,7 @@ public Builder variables(Map variables) { } public FieldCollectorParameters build() { - Assert.assertNotNull(graphQLSchema, "You must provide a schema"); + Assert.assertNotNull(graphQLSchema, () -> "You must provide a schema"); return new FieldCollectorParameters(this); } diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index a170ecb6e3..969ff4fd49 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -32,7 +32,7 @@ public enum CompleteValueType { } FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { - Assert.assertNotNull(fieldValueInfos, "fieldValueInfos can't be null"); + Assert.assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValue = fieldValue; this.fieldValueInfos = fieldValueInfos; diff --git a/src/main/java/graphql/execution/ResultPath.java b/src/main/java/graphql/execution/ResultPath.java index 9544ad57c7..28c37fcade 100644 --- a/src/main/java/graphql/execution/ResultPath.java +++ b/src/main/java/graphql/execution/ResultPath.java @@ -47,12 +47,12 @@ private ResultPath() { } private ResultPath(ResultPath parent, String segment) { - this.parent = Assert.assertNotNull(parent, "Must provide a parent path"); - this.segment = Assert.assertNotNull(segment, "Must provide a sub path"); + this.parent = Assert.assertNotNull(parent, () -> "Must provide a parent path"); + this.segment = Assert.assertNotNull(segment, () -> "Must provide a sub path"); } private ResultPath(ResultPath parent, int segment) { - this.parent = Assert.assertNotNull(parent, "Must provide a parent path"); + this.parent = Assert.assertNotNull(parent, () -> "Must provide a parent path"); this.segment = segment; } @@ -200,7 +200,7 @@ public ResultPath dropSegment() { * @return a new path with the last segment replaced */ public ResultPath replaceSegment(int segment) { - Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); + Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); return new ResultPath(parent, segment); } @@ -212,7 +212,7 @@ public ResultPath replaceSegment(int segment) { * @return a new path with the last segment replaced */ public ResultPath replaceSegment(String segment) { - Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); + Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); return new ResultPath(parent, segment); } diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index 16a91a942b..9385bab8c3 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -98,7 +98,7 @@ private CompletableFuture> createSourceEventStream(ExecutionCo return fieldFetched.thenApply(fetchedValue -> { Object publisher = fetchedValue.getFetchedValue(); if (publisher != null) { - Assert.assertTrue(publisher instanceof Publisher, "Your data fetcher must return a Publisher of events when using graphql subscriptions"); + Assert.assertTrue(publisher instanceof Publisher, () -> "Your data fetcher must return a Publisher of events when using graphql subscriptions"); } //noinspection unchecked return (Publisher) publisher; diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index a80a0a7358..3dbd929efc 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -283,7 +283,7 @@ private static Object externalValueToLiteralForObject( GraphQLContext graphqlContext, Locale locale ) { - Assert.assertTrue(inputValue instanceof Map, "Expect Map as input"); + Assert.assertTrue(inputValue instanceof Map, () -> "Expect Map as input"); Map inputMap = (Map) inputValue; List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); diff --git a/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java b/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java index 449e65f5d3..8f321d431a 100644 --- a/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java +++ b/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java @@ -38,7 +38,7 @@ class NonBlockingMutexExecutor implements Executor { @Override public void execute(final Runnable command) { - final RunNode newNode = new RunNode(Assert.assertNotNull(command, "Runnable must not be null")); + final RunNode newNode = new RunNode(Assert.assertNotNull(command, () -> "Runnable must not be null")); final RunNode prevLast = last.getAndSet(newNode); if (prevLast != null) prevLast.lazySet(newNode); diff --git a/src/main/java/graphql/extensions/ExtensionsBuilder.java b/src/main/java/graphql/extensions/ExtensionsBuilder.java index 8f731cf33b..bb28bf7472 100644 --- a/src/main/java/graphql/extensions/ExtensionsBuilder.java +++ b/src/main/java/graphql/extensions/ExtensionsBuilder.java @@ -108,7 +108,7 @@ public Map buildExtensions() { Map outMap = new LinkedHashMap<>(firstChange); for (int i = 1; i < changes.size(); i++) { Map newMap = extensionsMerger.merge(outMap, changes.get(i)); - Assert.assertNotNull(outMap, "You MUST provide a non null Map from ExtensionsMerger.merge()"); + Assert.assertNotNull(outMap, () -> "You MUST provide a non null Map from ExtensionsMerger.merge()"); outMap = newMap; } return outMap; diff --git a/src/main/java/graphql/language/AbstractNode.java b/src/main/java/graphql/language/AbstractNode.java index 736d23a98f..f097c9b491 100644 --- a/src/main/java/graphql/language/AbstractNode.java +++ b/src/main/java/graphql/language/AbstractNode.java @@ -25,9 +25,9 @@ public AbstractNode(SourceLocation sourceLocation, List comments, Ignor } public AbstractNode(SourceLocation sourceLocation, List comments, IgnoredChars ignoredChars, Map additionalData) { - Assert.assertNotNull(comments, "comments can't be null"); - Assert.assertNotNull(ignoredChars, "ignoredChars can't be null"); - Assert.assertNotNull(additionalData, "additionalData can't be null"); + Assert.assertNotNull(comments, () -> "comments can't be null"); + Assert.assertNotNull(ignoredChars, () -> "ignoredChars can't be null"); + Assert.assertNotNull(additionalData, () -> "additionalData can't be null"); this.sourceLocation = sourceLocation; this.additionalData = ImmutableMap.copyOf(additionalData); diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index e032c52c3b..1d1e717b73 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -457,7 +457,7 @@ private String node(Node node) { private String node(Node node, Class startClass) { if (startClass != null) { - Assert.assertTrue(startClass.isInstance(node), "The starting class must be in the inherit tree"); + Assert.assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); } StringBuilder builder = new StringBuilder(); NodePrinter printer = _findPrinter(node, startClass); diff --git a/src/main/java/graphql/language/NodeParentTree.java b/src/main/java/graphql/language/NodeParentTree.java index 616efadacc..da2dbfaacc 100644 --- a/src/main/java/graphql/language/NodeParentTree.java +++ b/src/main/java/graphql/language/NodeParentTree.java @@ -29,8 +29,8 @@ public class NodeParentTree { @Internal public NodeParentTree(Deque nodeStack) { - Assert.assertNotNull(nodeStack, "You MUST have a non null stack of nodes"); - Assert.assertTrue(!nodeStack.isEmpty(), "You MUST have a non empty stack of nodes"); + Assert.assertNotNull(nodeStack, () -> "You MUST have a non null stack of nodes"); + Assert.assertTrue(!nodeStack.isEmpty(), () -> "You MUST have a non empty stack of nodes"); Deque copy = new ArrayDeque<>(nodeStack); path = mkPath(copy); diff --git a/src/main/java/graphql/language/PrettyAstPrinter.java b/src/main/java/graphql/language/PrettyAstPrinter.java index 2a73b6cd8b..6ba895be98 100644 --- a/src/main/java/graphql/language/PrettyAstPrinter.java +++ b/src/main/java/graphql/language/PrettyAstPrinter.java @@ -221,7 +221,7 @@ private NodePrinter unionTypeDefinition(String nodeName) { private String node(Node node, Class startClass) { if (startClass != null) { - Assert.assertTrue(startClass.isInstance(node), "The starting class must be in the inherit tree"); + Assert.assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); } StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 01178b8160..c979d4a649 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -184,7 +184,7 @@ public boolean hasChildren() { public GraphQLOutputType getType(GraphQLSchema schema) { List fieldDefinitions = getFieldDefinitions(schema); Set fieldTypes = fieldDefinitions.stream().map(fd -> simplePrint(fd.getType())).collect(toSet()); - Assert.assertTrue(fieldTypes.size() == 1, "More than one type ... use getTypes"); + Assert.assertTrue(fieldTypes.size() == 1, () -> "More than one type ... use getTypes"); return fieldDefinitions.get(0).getType(); } @@ -438,7 +438,7 @@ public List getChildrenWithSameResultKey(String resul public List getChildren(int includingRelativeLevel) { List result = new ArrayList<>(); - Assert.assertTrue(includingRelativeLevel >= 1, "relative level must be >= 1"); + Assert.assertTrue(includingRelativeLevel >= 1, () -> "relative level must be >= 1"); this.getChildren().forEach(child -> { traverseImpl(child, result::add, 1, includingRelativeLevel); diff --git a/src/main/java/graphql/normalized/NormalizedInputValue.java b/src/main/java/graphql/normalized/NormalizedInputValue.java index 181e8594b1..1593d07135 100644 --- a/src/main/java/graphql/normalized/NormalizedInputValue.java +++ b/src/main/java/graphql/normalized/NormalizedInputValue.java @@ -101,7 +101,7 @@ private boolean isListOnly(String typeName) { private String unwrapOne(String typeName) { assertNotNull(typeName); - Assert.assertTrue(typeName.trim().length() > 0, "We have an empty type name unwrapped"); + Assert.assertTrue(typeName.trim().length() > 0, () -> "We have an empty type name unwrapped"); if (typeName.endsWith("!")) { return typeName.substring(0, typeName.length() - 1); } diff --git a/src/main/java/graphql/schema/AsyncDataFetcher.java b/src/main/java/graphql/schema/AsyncDataFetcher.java index fc405fe4b6..6fd93fae42 100644 --- a/src/main/java/graphql/schema/AsyncDataFetcher.java +++ b/src/main/java/graphql/schema/AsyncDataFetcher.java @@ -68,8 +68,8 @@ public AsyncDataFetcher(DataFetcher wrappedDataFetcher) { } public AsyncDataFetcher(DataFetcher wrappedDataFetcher, Executor executor) { - this.wrappedDataFetcher = Assert.assertNotNull(wrappedDataFetcher, "wrappedDataFetcher can't be null"); - this.executor = Assert.assertNotNull(executor, "executor can't be null"); + this.wrappedDataFetcher = Assert.assertNotNull(wrappedDataFetcher, () -> "wrappedDataFetcher can't be null"); + this.executor = Assert.assertNotNull(executor, () -> "executor can't be null"); } @Override diff --git a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java index c945ddf3c9..86b832109b 100644 --- a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java +++ b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java @@ -127,9 +127,9 @@ public static class Builder { * @return The {@code Builder} instance to allow chaining. */ public Builder addComparator(GraphqlTypeComparatorEnvironment environment, Class comparatorClass, Comparator comparator) { - Assert.assertNotNull(environment, "environment can't be null"); - Assert.assertNotNull(comparatorClass, "comparatorClass can't be null"); - Assert.assertNotNull(comparator, "comparator can't be null"); + Assert.assertNotNull(environment, () -> "environment can't be null"); + Assert.assertNotNull(comparatorClass, () -> "comparatorClass can't be null"); + Assert.assertNotNull(comparator, () -> "comparator can't be null"); registry.put(environment, comparator); return this; } diff --git a/src/main/java/graphql/schema/GraphQLTypeUtil.java b/src/main/java/graphql/schema/GraphQLTypeUtil.java index f33164937b..de5cd5e8b7 100644 --- a/src/main/java/graphql/schema/GraphQLTypeUtil.java +++ b/src/main/java/graphql/schema/GraphQLTypeUtil.java @@ -26,7 +26,7 @@ public class GraphQLTypeUtil { * @return the type in graphql SDL format, eg [typeName!]! */ public static String simplePrint(GraphQLType type) { - Assert.assertNotNull(type, "type can't be null"); + Assert.assertNotNull(type, () -> "type can't be null"); if (isNonNull(type)) { return simplePrint(unwrapOne(type)) + "!"; } else if (isList(type)) { diff --git a/src/main/java/graphql/schema/InputValueWithState.java b/src/main/java/graphql/schema/InputValueWithState.java index 0a8ed45f0a..46ed2ec134 100644 --- a/src/main/java/graphql/schema/InputValueWithState.java +++ b/src/main/java/graphql/schema/InputValueWithState.java @@ -46,7 +46,7 @@ private InputValueWithState(State state, Object value) { public static final InputValueWithState NOT_SET = new InputValueWithState(State.NOT_SET, null); public static InputValueWithState newLiteralValue(@NotNull Value value) { - Assert.assertNotNull(value, "value literal can't be null"); + Assert.assertNotNull(value, () -> "value literal can't be null"); return new InputValueWithState(State.LITERAL, value); } From 0449ee9c211f38f58de3b59fe3c439348d0e29cc Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 7 Mar 2024 22:16:44 +1100 Subject: [PATCH 297/393] reverting back to pre code change --- .../java/graphql/GraphqlErrorBuilder.java | 3 +-- src/main/java/graphql/execution/Async.java | 8 +++--- .../graphql/execution/ExecutionStepInfo.java | 7 +++--- .../ExecutionStrategyParameters.java | 7 +++--- .../graphql/execution/FieldValueInfo.java | 3 +-- .../java/graphql/execution/ResultPath.java | 23 ++++++++++------- .../SubscriptionExecutionStrategy.java | 3 +-- .../execution/ValuesResolverConversion.java | 6 ++--- .../reactive/NonBlockingMutexExecutor.java | 25 ++++++++++--------- .../graphql/extensions/ExtensionsBuilder.java | 4 +-- .../java/graphql/language/AstPrinter.java | 11 ++++---- .../java/graphql/language/NodeParentTree.java | 5 ++-- .../graphql/language/PrettyAstPrinter.java | 7 +++--- .../normalized/ExecutableNormalizedField.java | 6 ++--- .../java/graphql/schema/AsyncDataFetcher.java | 5 ++-- .../DefaultGraphqlTypeComparatorRegistry.java | 8 +++--- .../graphql/schema/InputValueWithState.java | 3 +-- 17 files changed, 65 insertions(+), 69 deletions(-) diff --git a/src/main/java/graphql/GraphqlErrorBuilder.java b/src/main/java/graphql/GraphqlErrorBuilder.java index ae4ad5095f..4cef5beabe 100644 --- a/src/main/java/graphql/GraphqlErrorBuilder.java +++ b/src/main/java/graphql/GraphqlErrorBuilder.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Map; -import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; /** @@ -129,7 +128,7 @@ public B extensions(@Nullable Map extensions) { * @return a newly built GraphqlError */ public GraphQLError build() { - Assert.assertNotNull(message,() -> "You must provide error message"); + assertNotNull(message, () -> "You must provide error message"); return new GraphqlErrorImpl(message, locations, errorType, path, extensions); } diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index 498acaba6f..b1f1dc36d4 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -17,6 +17,8 @@ import java.util.function.Function; import java.util.function.Supplier; +import static graphql.Assert.assertTrue; + @Internal @SuppressWarnings("FutureReturnValueIgnored") public class Async { @@ -58,7 +60,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == 0, () -> "expected size was 0"); + assertTrue(ix == 0, "expected size was 0 got %d", ix); return typedEmpty(); } @@ -86,7 +88,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == 1, "expected size was 1 got %d", ix); + assertTrue(ix == 1, "expected size was 1 got %d", ix); return completableFuture.thenApply(Collections::singletonList); } } @@ -109,7 +111,7 @@ public void add(CompletableFuture completableFuture) { @Override public CompletableFuture> await() { - Assert.assertTrue(ix == array.length, "expected size was %d got %d", array.length, ix); + assertTrue(ix == array.length, "expected size was %d got %d", array.length, ix); CompletableFuture> overallResult = new CompletableFuture<>(); CompletableFuture.allOf(array) diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index c7074a5c7f..08836d977f 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfo.java +++ b/src/main/java/graphql/execution/ExecutionStepInfo.java @@ -1,6 +1,5 @@ package graphql.execution; -import graphql.Assert; import graphql.PublicApi; import graphql.collect.ImmutableMapWithNullValues; import graphql.schema.GraphQLFieldDefinition; @@ -14,6 +13,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertTrue; import static graphql.schema.GraphQLTypeUtil.isList; /** @@ -71,7 +72,7 @@ private ExecutionStepInfo(Builder builder) { this.field = builder.field; this.path = builder.path; this.parent = builder.parentInfo; - this.type = Assert.assertNotNull(builder.type, () -> "you must provide a graphql type"); + this.type = assertNotNull(builder.type, () -> "you must provide a graphql type"); this.arguments = builder.arguments; this.fieldContainer = builder.fieldContainer; } @@ -201,7 +202,7 @@ public boolean hasParent() { * @return a new type info with the same */ public ExecutionStepInfo changeTypeWithPreservedNonNull(GraphQLOutputType newType) { - Assert.assertTrue(!GraphQLTypeUtil.isNonNull(newType), () -> "newType can't be non null"); + assertTrue(!GraphQLTypeUtil.isNonNull(newType), () -> "newType can't be non null"); if (isNonNullType()) { return newExecutionStepInfo(this).type(GraphQLNonNull.nonNull(newType)).build(); } else { diff --git a/src/main/java/graphql/execution/ExecutionStrategyParameters.java b/src/main/java/graphql/execution/ExecutionStrategyParameters.java index 915a4cfab6..e1df7bb363 100644 --- a/src/main/java/graphql/execution/ExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/ExecutionStrategyParameters.java @@ -1,6 +1,5 @@ package graphql.execution; -import graphql.Assert; import graphql.PublicApi; import graphql.execution.incremental.DeferredCallContext; @@ -33,9 +32,9 @@ private ExecutionStrategyParameters(ExecutionStepInfo executionStepInfo, ExecutionStrategyParameters parent, DeferredCallContext deferredCallContext) { - this.executionStepInfo = Assert.assertNotNull(executionStepInfo, () -> "executionStepInfo is null"); + this.executionStepInfo = assertNotNull(executionStepInfo, () -> "executionStepInfo is null"); this.localContext = localContext; - this.fields = Assert.assertNotNull(fields, () -> "fields is null"); + this.fields = assertNotNull(fields, () -> "fields is null"); this.source = source; this.nonNullableFieldValidator = nonNullableFieldValidator; this.path = path; @@ -168,7 +167,7 @@ public Builder localContext(Object localContext) { } public Builder nonNullFieldValidator(NonNullableFieldValidator nonNullableFieldValidator) { - this.nonNullableFieldValidator = Assert.assertNotNull(nonNullableFieldValidator, () -> "requires a NonNullValidator"); + this.nonNullableFieldValidator = assertNotNull(nonNullableFieldValidator, () -> "requires a NonNullValidator"); return this; } diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index 969ff4fd49..c13e915936 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -1,7 +1,6 @@ package graphql.execution; import com.google.common.collect.ImmutableList; -import graphql.Assert; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.PublicApi; @@ -32,7 +31,7 @@ public enum CompleteValueType { } FieldValueInfo(CompleteValueType completeValueType, CompletableFuture fieldValue, List fieldValueInfos) { - Assert.assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); + assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValue = fieldValue; this.fieldValueInfos = fieldValueInfos; diff --git a/src/main/java/graphql/execution/ResultPath.java b/src/main/java/graphql/execution/ResultPath.java index 28c37fcade..472b5fa529 100644 --- a/src/main/java/graphql/execution/ResultPath.java +++ b/src/main/java/graphql/execution/ResultPath.java @@ -1,7 +1,6 @@ package graphql.execution; import com.google.common.collect.ImmutableList; -import graphql.Assert; import graphql.AssertException; import graphql.PublicApi; import graphql.collect.ImmutableKit; @@ -12,7 +11,6 @@ import java.util.Objects; import java.util.StringTokenizer; -import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; import static graphql.Assert.assertTrue; import static java.lang.String.format; @@ -47,12 +45,12 @@ private ResultPath() { } private ResultPath(ResultPath parent, String segment) { - this.parent = Assert.assertNotNull(parent, () -> "Must provide a parent path"); - this.segment = Assert.assertNotNull(segment, () -> "Must provide a sub path"); + this.parent = assertNotNull(parent, () -> "Must provide a parent path"); + this.segment = assertNotNull(segment, () -> "Must provide a sub path"); } private ResultPath(ResultPath parent, int segment) { - this.parent = Assert.assertNotNull(parent, () -> "Must provide a parent path"); + this.parent = assertNotNull(parent, () -> "Must provide a parent path"); this.segment = segment; } @@ -113,6 +111,7 @@ public ResultPath getParent() { * Parses an execution path from the provided path string in the format /segment1/segment2[index]/segmentN * * @param pathString the path string + * * @return a parsed execution path */ public static ResultPath parse(String pathString) { @@ -141,6 +140,7 @@ public static ResultPath parse(String pathString) { * This will create an execution path from the list of objects * * @param objects the path objects + * * @return a new execution path */ public static ResultPath fromList(List objects) { @@ -164,6 +164,7 @@ private static String mkErrMsg() { * Takes the current path and adds a new segment to it, returning a new path * * @param segment the string path segment to add + * * @return a new path containing that segment */ public ResultPath segment(String segment) { @@ -174,6 +175,7 @@ public ResultPath segment(String segment) { * Takes the current path and adds a new segment to it, returning a new path * * @param segment the int path segment to add + * * @return a new path containing that segment */ public ResultPath segment(int segment) { @@ -197,10 +199,11 @@ public ResultPath dropSegment() { * equals "/a/b[9]" * * @param segment the integer segment to use + * * @return a new path with the last segment replaced */ public ResultPath replaceSegment(int segment) { - Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); + assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); return new ResultPath(parent, segment); } @@ -209,10 +212,11 @@ public ResultPath replaceSegment(int segment) { * equals "/a/b/x" * * @param segment the string segment to use + * * @return a new path with the last segment replaced */ public ResultPath replaceSegment(String segment) { - Assert.assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); + assertTrue(!ROOT_PATH.equals(this), () -> "You MUST not call this with the root path"); return new ResultPath(parent, segment); } @@ -228,6 +232,7 @@ public boolean isRootPath() { * Appends the provided path to the current one * * @param path the path to append + * * @return a new path */ public ResultPath append(ResultPath path) { @@ -238,12 +243,12 @@ public ResultPath append(ResultPath path) { public ResultPath sibling(String siblingField) { - Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); + assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); return new ResultPath(this.parent, siblingField); } public ResultPath sibling(int siblingField) { - Assert.assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); + assertTrue(!ROOT_PATH.equals(this), "You MUST not call this with the root path"); return new ResultPath(this.parent, siblingField); } diff --git a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java index 9385bab8c3..f735b0dc6a 100644 --- a/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java +++ b/src/main/java/graphql/execution/SubscriptionExecutionStrategy.java @@ -1,6 +1,5 @@ package graphql.execution; -import graphql.Assert; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.PublicApi; @@ -98,7 +97,7 @@ private CompletableFuture> createSourceEventStream(ExecutionCo return fieldFetched.thenApply(fetchedValue -> { Object publisher = fetchedValue.getFetchedValue(); if (publisher != null) { - Assert.assertTrue(publisher instanceof Publisher, () -> "Your data fetcher must return a Publisher of events when using graphql subscriptions"); + assertTrue(publisher instanceof Publisher, () -> "Your data fetcher must return a Publisher of events when using graphql subscriptions"); } //noinspection unchecked return (Publisher) publisher; diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index 3dbd929efc..eff4d1cef1 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -1,7 +1,6 @@ package graphql.execution; import com.google.common.collect.ImmutableList; -import graphql.Assert; import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.values.InputInterceptor; @@ -39,7 +38,6 @@ import static graphql.Assert.assertShouldNeverHappen; import static graphql.Assert.assertTrue; -import static graphql.Assert.assertTrue; import static graphql.collect.ImmutableKit.emptyList; import static graphql.collect.ImmutableKit.map; import static graphql.execution.ValuesResolver.ValueMode.NORMALIZED; @@ -106,6 +104,7 @@ static Object valueToLiteralImpl(GraphqlFieldVisibility fieldVisibility, * @param type the type of input value * @param graphqlContext the GraphqlContext to use * @param locale the Locale to use + * * @return a value converted to an internal value */ static Object externalValueToInternalValue(GraphqlFieldVisibility fieldVisibility, @@ -283,7 +282,7 @@ private static Object externalValueToLiteralForObject( GraphQLContext graphqlContext, Locale locale ) { - Assert.assertTrue(inputValue instanceof Map, () -> "Expect Map as input"); + assertTrue(inputValue instanceof Map, () -> "Expect Map as input"); Map inputMap = (Map) inputValue; List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); @@ -597,6 +596,7 @@ private static List externalValueToInternalValueForList( * @param coercedVariables the coerced variable values * @param graphqlContext the GraphqlContext to use * @param locale the Locale to use + * * @return literal converted to an internal value */ static Object literalToInternalValue( diff --git a/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java b/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java index 8f321d431a..38c6eb1238 100644 --- a/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java +++ b/src/main/java/graphql/execution/reactive/NonBlockingMutexExecutor.java @@ -1,8 +1,8 @@ package graphql.execution.reactive; -import graphql.Assert; import graphql.Internal; +import org.jetbrains.annotations.NotNull; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; @@ -13,37 +13,38 @@ /** * Executor that provides mutual exclusion between the operations submitted to it, * without blocking. - * + *

* If an operation is submitted to this executor while no other operation is * running, it will run immediately. - * + *

* If an operation is submitted to this executor while another operation is * running, it will be added to a queue of operations to run, and the executor will * return. The thread currently running an operation will end up running the * operation just submitted. - * + *

* Operations submitted to this executor should run fast, as they can end up running * on other threads and interfere with the operation of other threads. - * + *

* This executor can also be used to address infinite recursion problems, as * operations submitted recursively will run sequentially. + *

* - * - * Inspired by Public Domain CC0 code at h - * https://github.com/jroper/reactive-streams-servlet/tree/master/reactive-streams-servlet/src/main/java/org/reactivestreams/servlet + * Inspired by Public Domain CC0 code at + * ... */ @Internal class NonBlockingMutexExecutor implements Executor { private final AtomicReference last = new AtomicReference<>(); @Override - public void execute(final Runnable command) { - final RunNode newNode = new RunNode(Assert.assertNotNull(command, () -> "Runnable must not be null")); + public void execute(final @NotNull Runnable command) { + final RunNode newNode = new RunNode(assertNotNull(command, () -> "Runnable must not be null")); final RunNode prevLast = last.getAndSet(newNode); - if (prevLast != null) + if (prevLast != null) { prevLast.lazySet(newNode); - else + } else { runAll(newNode); + } } private void reportFailure(final Thread runner, final Throwable thrown) { diff --git a/src/main/java/graphql/extensions/ExtensionsBuilder.java b/src/main/java/graphql/extensions/ExtensionsBuilder.java index bb28bf7472..fe37c64743 100644 --- a/src/main/java/graphql/extensions/ExtensionsBuilder.java +++ b/src/main/java/graphql/extensions/ExtensionsBuilder.java @@ -1,7 +1,6 @@ package graphql.extensions; import com.google.common.collect.ImmutableMap; -import graphql.Assert; import graphql.ExecutionResult; import graphql.PublicApi; import org.jetbrains.annotations.NotNull; @@ -13,7 +12,6 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; /** @@ -108,7 +106,7 @@ public Map buildExtensions() { Map outMap = new LinkedHashMap<>(firstChange); for (int i = 1; i < changes.size(); i++) { Map newMap = extensionsMerger.merge(outMap, changes.get(i)); - Assert.assertNotNull(outMap, () -> "You MUST provide a non null Map from ExtensionsMerger.merge()"); + assertNotNull(outMap, () -> "You MUST provide a non null Map from ExtensionsMerger.merge()"); outMap = newMap; } return outMap; diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index 1d1e717b73..b48fa2075d 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -1,7 +1,5 @@ package graphql.language; -import graphql.Assert; -import graphql.AssertException; import graphql.PublicApi; import graphql.collect.ImmutableKit; @@ -457,7 +455,7 @@ private String node(Node node) { private String node(Node node, Class startClass) { if (startClass != null) { - Assert.assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); + assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); } StringBuilder builder = new StringBuilder(); NodePrinter printer = _findPrinter(node, startClass); @@ -543,7 +541,7 @@ private String description(Node node) { } private String directives(List directives) { - return join(nvl(directives), compactMode? "" : " "); + return join(nvl(directives), compactMode ? "" : " "); } private String join(List nodes, String delim) { @@ -566,7 +564,7 @@ private String joinTight(List nodes, String delim, String pr first = false; } else { boolean canButtTogether = lastNodeText.endsWith("}"); - if (! canButtTogether) { + if (!canButtTogether) { joined.append(delim); } } @@ -706,7 +704,8 @@ interface NodePrinter { /** * Allow subclasses to replace a printer for a specific {@link Node} - * @param nodeClass the class of the {@link Node} + * + * @param nodeClass the class of the {@link Node} * @param nodePrinter the custom {@link NodePrinter} */ void replacePrinter(Class nodeClass, NodePrinter nodePrinter) { diff --git a/src/main/java/graphql/language/NodeParentTree.java b/src/main/java/graphql/language/NodeParentTree.java index da2dbfaacc..fc78ea093d 100644 --- a/src/main/java/graphql/language/NodeParentTree.java +++ b/src/main/java/graphql/language/NodeParentTree.java @@ -1,7 +1,6 @@ package graphql.language; import com.google.common.collect.ImmutableList; -import graphql.Assert; import graphql.Internal; import graphql.PublicApi; @@ -29,8 +28,8 @@ public class NodeParentTree { @Internal public NodeParentTree(Deque nodeStack) { - Assert.assertNotNull(nodeStack, () -> "You MUST have a non null stack of nodes"); - Assert.assertTrue(!nodeStack.isEmpty(), () -> "You MUST have a non empty stack of nodes"); + assertNotNull(nodeStack, () -> "You MUST have a non null stack of nodes"); + assertTrue(!nodeStack.isEmpty(), () -> "You MUST have a non empty stack of nodes"); Deque copy = new ArrayDeque<>(nodeStack); path = mkPath(copy); diff --git a/src/main/java/graphql/language/PrettyAstPrinter.java b/src/main/java/graphql/language/PrettyAstPrinter.java index 6ba895be98..c763a7b93d 100644 --- a/src/main/java/graphql/language/PrettyAstPrinter.java +++ b/src/main/java/graphql/language/PrettyAstPrinter.java @@ -1,6 +1,5 @@ package graphql.language; -import graphql.Assert; import graphql.ExperimentalApi; import graphql.collect.ImmutableKit; import graphql.parser.CommentParser; @@ -221,7 +220,7 @@ private NodePrinter unionTypeDefinition(String nodeName) { private String node(Node node, Class startClass) { if (startClass != null) { - Assert.assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); + assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); } StringBuilder builder = new StringBuilder(); @@ -409,10 +408,10 @@ public static class PrettyPrinterOptions { private static final PrettyPrinterOptions defaultOptions = new PrettyPrinterOptions(IndentType.SPACE, 2); private PrettyPrinterOptions(IndentType indentType, int indentWidth) { - this.indentText = String.join("", Collections.nCopies(indentWidth, indentType.character)); + this.indentText = String.join("", Collections.nCopies(indentWidth, indentType.character)); } - public static PrettyPrinterOptions defaultOptions() { + public static PrettyPrinterOptions defaultOptions() { return defaultOptions; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index c979d4a649..f9db04b00a 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import graphql.Assert; import graphql.ExperimentalApi; import graphql.Internal; import graphql.Mutable; @@ -184,7 +183,7 @@ public boolean hasChildren() { public GraphQLOutputType getType(GraphQLSchema schema) { List fieldDefinitions = getFieldDefinitions(schema); Set fieldTypes = fieldDefinitions.stream().map(fd -> simplePrint(fd.getType())).collect(toSet()); - Assert.assertTrue(fieldTypes.size() == 1, () -> "More than one type ... use getTypes"); + assertTrue(fieldTypes.size() == 1, () -> "More than one type ... use getTypes"); return fieldDefinitions.get(0).getType(); } @@ -438,7 +437,7 @@ public List getChildrenWithSameResultKey(String resul public List getChildren(int includingRelativeLevel) { List result = new ArrayList<>(); - Assert.assertTrue(includingRelativeLevel >= 1, () -> "relative level must be >= 1"); + assertTrue(includingRelativeLevel >= 1, () -> "relative level must be >= 1"); this.getChildren().forEach(child -> { traverseImpl(child, result::add, 1, includingRelativeLevel); @@ -478,6 +477,7 @@ public ExecutableNormalizedField getParent() { /** * @return the {@link NormalizedDeferredExecution}s associated with this {@link ExecutableNormalizedField}. + * * @see NormalizedDeferredExecution */ @ExperimentalApi diff --git a/src/main/java/graphql/schema/AsyncDataFetcher.java b/src/main/java/graphql/schema/AsyncDataFetcher.java index 6fd93fae42..b07aa80ddb 100644 --- a/src/main/java/graphql/schema/AsyncDataFetcher.java +++ b/src/main/java/graphql/schema/AsyncDataFetcher.java @@ -1,6 +1,5 @@ package graphql.schema; -import graphql.Assert; import graphql.PublicApi; import java.util.concurrent.CompletableFuture; @@ -68,8 +67,8 @@ public AsyncDataFetcher(DataFetcher wrappedDataFetcher) { } public AsyncDataFetcher(DataFetcher wrappedDataFetcher, Executor executor) { - this.wrappedDataFetcher = Assert.assertNotNull(wrappedDataFetcher, () -> "wrappedDataFetcher can't be null"); - this.executor = Assert.assertNotNull(executor, () -> "executor can't be null"); + this.wrappedDataFetcher = assertNotNull(wrappedDataFetcher, () -> "wrappedDataFetcher can't be null"); + this.executor = assertNotNull(executor, () -> "executor can't be null"); } @Override diff --git a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java index 86b832109b..3474dd5b7c 100644 --- a/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java +++ b/src/main/java/graphql/schema/DefaultGraphqlTypeComparatorRegistry.java @@ -1,7 +1,6 @@ package graphql.schema; import com.google.common.collect.ImmutableMap; -import graphql.Assert; import graphql.PublicApi; import java.util.Comparator; @@ -10,7 +9,6 @@ import java.util.Objects; import java.util.function.UnaryOperator; -import static graphql.Assert.assertNotNull; import static graphql.Assert.assertNotNull; import static graphql.schema.GraphQLTypeUtil.unwrapAll; import static graphql.schema.GraphqlTypeComparatorEnvironment.newEnvironment; @@ -127,9 +125,9 @@ public static class Builder { * @return The {@code Builder} instance to allow chaining. */ public Builder addComparator(GraphqlTypeComparatorEnvironment environment, Class comparatorClass, Comparator comparator) { - Assert.assertNotNull(environment, () -> "environment can't be null"); - Assert.assertNotNull(comparatorClass, () -> "comparatorClass can't be null"); - Assert.assertNotNull(comparator, () -> "comparator can't be null"); + assertNotNull(environment, () -> "environment can't be null"); + assertNotNull(comparatorClass, () -> "comparatorClass can't be null"); + assertNotNull(comparator, () -> "comparator can't be null"); registry.put(environment, comparator); return this; } diff --git a/src/main/java/graphql/schema/InputValueWithState.java b/src/main/java/graphql/schema/InputValueWithState.java index 46ed2ec134..33ef45d062 100644 --- a/src/main/java/graphql/schema/InputValueWithState.java +++ b/src/main/java/graphql/schema/InputValueWithState.java @@ -1,6 +1,5 @@ package graphql.schema; -import graphql.Assert; import graphql.PublicApi; import graphql.language.Value; import org.jetbrains.annotations.NotNull; @@ -46,7 +45,7 @@ private InputValueWithState(State state, Object value) { public static final InputValueWithState NOT_SET = new InputValueWithState(State.NOT_SET, null); public static InputValueWithState newLiteralValue(@NotNull Value value) { - Assert.assertNotNull(value, () -> "value literal can't be null"); + assertNotNull(value, () -> "value literal can't be null"); return new InputValueWithState(State.LITERAL, value); } From 254009cce4c2f3a26d8b9ef3fd7acb5de3e0ae62 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 7 Mar 2024 22:27:30 +1100 Subject: [PATCH 298/393] better import --- src/test/groovy/graphql/AssertTest.groovy | 74 ++++++++++++----------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/test/groovy/graphql/AssertTest.groovy b/src/test/groovy/graphql/AssertTest.groovy index 5d6901ba23..fe6d818f1f 100644 --- a/src/test/groovy/graphql/AssertTest.groovy +++ b/src/test/groovy/graphql/AssertTest.groovy @@ -2,10 +2,12 @@ package graphql import spock.lang.Specification +import static graphql.Assert.* + class AssertTest extends Specification { def "assertNotNull should not throw on none null value"() { when: - Assert.assertNotNull("some object") + assertNotNull("some object") then: noExceptionThrown() @@ -13,7 +15,7 @@ class AssertTest extends Specification { def "assertNotNull should throw on null value"() { when: - Assert.assertNotNull(null) + assertNotNull(null) then: thrown(AssertException) @@ -21,7 +23,7 @@ class AssertTest extends Specification { def "assertNotNull constant message should throw on null value"() { when: - Assert.assertNotNull(null, "constant message") + assertNotNull(null, "constant message") then: def error = thrown(AssertException) @@ -30,7 +32,7 @@ class AssertTest extends Specification { def "assertNotNull with error message should not throw on none null value"() { when: - Assert.assertNotNull("some object", { -> "error message" }) + assertNotNull("some object", { -> "error message" }) then: noExceptionThrown() @@ -38,7 +40,7 @@ class AssertTest extends Specification { def "assertNotNull with error message should throw on null value with formatted message"() { when: - Assert.assertNotNull(value, { -> String.format(format, arg) }) + assertNotNull(value, { -> String.format(format, arg) }) then: def error = thrown(AssertException) @@ -61,9 +63,9 @@ class AssertTest extends Specification { where: toRun | expectedMessage - runnable({ Assert.assertNotNull(null, "error %s", "arg1") }) | "error arg1" - runnable({ Assert.assertNotNull(null, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" - runnable({ Assert.assertNotNull(null, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + runnable({ assertNotNull(null, "error %s", "arg1") }) | "error arg1" + runnable({ assertNotNull(null, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ assertNotNull(null, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" } def "assertNotNull with different number of error args with non null does not throw assertions"() { @@ -75,14 +77,14 @@ class AssertTest extends Specification { where: toRun | expectedMessage - runnable({ Assert.assertNotNull("x", "error %s", "arg1") }) | "error arg1" - runnable({ Assert.assertNotNull("x", "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" - runnable({ Assert.assertNotNull("x", "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + runnable({ assertNotNull("x", "error %s", "arg1") }) | "error arg1" + runnable({ assertNotNull("x", "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ assertNotNull("x", "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" } def "assertNeverCalled should always throw"() { when: - Assert.assertNeverCalled() + assertNeverCalled() then: def e = thrown(AssertException) @@ -91,7 +93,7 @@ class AssertTest extends Specification { def "assertShouldNeverHappen should always throw"() { when: - Assert.assertShouldNeverHappen() + assertShouldNeverHappen() then: def e = thrown(AssertException) @@ -100,7 +102,7 @@ class AssertTest extends Specification { def "assertShouldNeverHappen should always throw with formatted message"() { when: - Assert.assertShouldNeverHappen(format, arg) + assertShouldNeverHappen(format, arg) then: def error = thrown(AssertException) @@ -115,7 +117,7 @@ class AssertTest extends Specification { def "assertNotEmpty collection should throw on null or empty"() { when: - Assert.assertNotEmpty(value, { -> String.format(format, arg) }) + assertNotEmpty(value, { -> String.format(format, arg) }) then: def error = thrown(AssertException) @@ -129,7 +131,7 @@ class AssertTest extends Specification { def "assertNotEmpty should not throw on none empty collection"() { when: - Assert.assertNotEmpty(["some object"], { -> "error message" }) + assertNotEmpty(["some object"], { -> "error message" }) then: noExceptionThrown() @@ -137,7 +139,7 @@ class AssertTest extends Specification { def "assertTrue should not throw on true value"() { when: - Assert.assertTrue(true, { -> "error message" }) + assertTrue(true, { -> "error message" }) then: noExceptionThrown() @@ -145,7 +147,7 @@ class AssertTest extends Specification { def "assertTrue with error message should throw on false value with formatted message"() { when: - Assert.assertTrue(false, { -> String.format(format, arg) }) + assertTrue(false, { -> String.format(format, arg) }) then: def error = thrown(AssertException) @@ -160,7 +162,7 @@ class AssertTest extends Specification { def "assertTrue constant message should throw with message"() { when: - Assert.assertTrue(false, "constant message") + assertTrue(false, "constant message") then: def error = thrown(AssertException) @@ -177,9 +179,9 @@ class AssertTest extends Specification { where: toRun | expectedMessage - runnable({ Assert.assertTrue(false, "error %s", "arg1") }) | "error arg1" - runnable({ Assert.assertTrue(false, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" - runnable({ Assert.assertTrue(false, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + runnable({ assertTrue(false, "error %s", "arg1") }) | "error arg1" + runnable({ assertTrue(false, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ assertTrue(false, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" } def "assertTrue with different number of error args but false does not throw assertions"() { @@ -191,14 +193,14 @@ class AssertTest extends Specification { where: toRun | expectedMessage - runnable({ Assert.assertTrue(true, "error %s", "arg1") }) | "error arg1" - runnable({ Assert.assertTrue(true, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" - runnable({ Assert.assertTrue(true, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + runnable({ assertTrue(true, "error %s", "arg1") }) | "error arg1" + runnable({ assertTrue(true, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ assertTrue(true, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" } def "assertFalse should throw"() { when: - Assert.assertFalse(true) + assertFalse(true) then: thrown(AssertException) @@ -206,7 +208,7 @@ class AssertTest extends Specification { def "assertFalse constant message should throw with message"() { when: - Assert.assertFalse(true, "constant message") + assertFalse(true, "constant message") then: def error = thrown(AssertException) @@ -215,7 +217,7 @@ class AssertTest extends Specification { def "assertFalse with error message should throw on false value with formatted message"() { when: - Assert.assertFalse(true, { -> String.format(format, arg) }) + assertFalse(true, { -> String.format(format, arg) }) then: def error = thrown(AssertException) @@ -238,9 +240,9 @@ class AssertTest extends Specification { where: toRun | expectedMessage - runnable({ Assert.assertFalse(true, "error %s", "arg1") }) | "error arg1" - runnable({ Assert.assertFalse(true, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" - runnable({ Assert.assertFalse(true, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + runnable({ assertFalse(true, "error %s", "arg1") }) | "error arg1" + runnable({ assertFalse(true, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ assertFalse(true, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" } def "assertFalse with different number of error args but false does not throw assertions"() { @@ -252,14 +254,14 @@ class AssertTest extends Specification { where: toRun | expectedMessage - runnable({ Assert.assertFalse(false, "error %s", "arg1") }) | "error arg1" - runnable({ Assert.assertFalse(false, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" - runnable({ Assert.assertFalse(false, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" + runnable({ assertFalse(false, "error %s", "arg1") }) | "error arg1" + runnable({ assertFalse(false, "error %s %s", "arg1", "arg2") }) | "error arg1 arg2" + runnable({ assertFalse(false, "error %s %s %s", "arg1", "arg2", "arg3") }) | "error arg1 arg2 arg3" } def "assertValidName should not throw on valid names"() { when: - Assert.assertValidName(name) + assertValidName(name) then: noExceptionThrown() @@ -275,7 +277,7 @@ class AssertTest extends Specification { def "assertValidName should throw on invalid names"() { when: - Assert.assertValidName(name) + assertValidName(name) then: def error = thrown(AssertException) From bae150f8473a1625fd23696f2cf176de8ed26603 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 7 Mar 2024 23:15:33 +1100 Subject: [PATCH 299/393] re-wrote i18n corcing asserts --- src/main/java/graphql/scalar/GraphqlBooleanCoercing.java | 6 ++++-- src/main/java/graphql/scalar/GraphqlFloatCoercing.java | 7 +++++-- src/main/java/graphql/scalar/GraphqlIDCoercing.java | 6 +++++- src/main/java/graphql/scalar/GraphqlIntCoercing.java | 7 +++++-- src/main/java/graphql/schema/GraphQLEnumType.java | 4 +++- src/test/java/benchmark/AssertBenchmark.java | 6 +++--- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java index 266e64c4a5..ceddc7558c 100644 --- a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java @@ -15,7 +15,6 @@ import java.math.BigDecimal; import java.util.Locale; -import static graphql.Assert.assertNotNull; import static graphql.Assert.assertShouldNeverHappen; import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.isNumberIsh; @@ -88,7 +87,10 @@ private static boolean parseLiteralImpl(@NotNull Object input, @NotNull Locale l @NotNull private BooleanValue valueToLiteralImpl(@NotNull Object input, @NotNull Locale locale) { - Boolean result = assertNotNull(convertImpl(input), () -> i18nMsg(locale, "Boolean.notBoolean", typeName(input))); + Boolean result = convertImpl(input); + if (result == null) { + assertShouldNeverHappen(i18nMsg(locale, "Boolean.notBoolean", typeName(input))); + } return BooleanValue.newBooleanValue(result).build(); } diff --git a/src/main/java/graphql/scalar/GraphqlFloatCoercing.java b/src/main/java/graphql/scalar/GraphqlFloatCoercing.java index 58329e56f3..e5e3526e8a 100644 --- a/src/main/java/graphql/scalar/GraphqlFloatCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlFloatCoercing.java @@ -16,7 +16,7 @@ import java.math.BigDecimal; import java.util.Locale; -import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.isNumberIsh; import static graphql.scalar.CoercingUtil.typeName; @@ -89,7 +89,10 @@ private static double parseLiteralImpl(@NotNull Object input, @NotNull Locale lo @NotNull private FloatValue valueToLiteralImpl(Object input, @NotNull Locale locale) { - Double result = assertNotNull(convertImpl(input), () -> i18nMsg(locale, "Float.notFloat", typeName(input))); + Double result = convertImpl(input); + if (result == null) { + assertShouldNeverHappen(i18nMsg(locale, "Float.notFloat", typeName(input))); + } return FloatValue.newFloatValue(BigDecimal.valueOf(result)).build(); } diff --git a/src/main/java/graphql/scalar/GraphqlIDCoercing.java b/src/main/java/graphql/scalar/GraphqlIDCoercing.java index 76e78e7917..4631c93c5d 100644 --- a/src/main/java/graphql/scalar/GraphqlIDCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlIDCoercing.java @@ -18,6 +18,7 @@ import java.util.UUID; import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.typeName; @@ -84,7 +85,10 @@ private String parseLiteralImpl(Object input, @NotNull Locale locale) { @NotNull private StringValue valueToLiteralImpl(Object input, @NotNull Locale locale) { - String result = assertNotNull(convertImpl(input), () -> i18nMsg(locale, "ID.notId", typeName(input))); + String result = convertImpl(input); + if (result == null) { + assertShouldNeverHappen(i18nMsg(locale, "ID.notId", typeName(input))); + } return StringValue.newStringValue(result).build(); } diff --git a/src/main/java/graphql/scalar/GraphqlIntCoercing.java b/src/main/java/graphql/scalar/GraphqlIntCoercing.java index 99e9eeb84b..0a4a055883 100644 --- a/src/main/java/graphql/scalar/GraphqlIntCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlIntCoercing.java @@ -16,7 +16,7 @@ import java.math.BigInteger; import java.util.Locale; -import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.isNumberIsh; import static graphql.scalar.CoercingUtil.typeName; @@ -91,7 +91,10 @@ private static int parseLiteralImpl(Object input, @NotNull Locale locale) { } private IntValue valueToLiteralImpl(Object input, @NotNull Locale locale) { - Integer result = assertNotNull(convertImpl(input),() -> i18nMsg(locale, "Int.notInt", typeName(input))); + Integer result = convertImpl(input); + if (result == null) { + assertShouldNeverHappen(i18nMsg(locale, "Int.notInt", typeName(input))); + } return IntValue.newIntValue(BigInteger.valueOf(result)).build(); } diff --git a/src/main/java/graphql/schema/GraphQLEnumType.java b/src/main/java/graphql/schema/GraphQLEnumType.java index 00ced1b451..f6aa8d49ed 100644 --- a/src/main/java/graphql/schema/GraphQLEnumType.java +++ b/src/main/java/graphql/schema/GraphQLEnumType.java @@ -130,7 +130,9 @@ public Value valueToLiteral(Object input) { @Internal public Value valueToLiteral(Object input, GraphQLContext graphQLContext, Locale locale) { GraphQLEnumValueDefinition enumValueDefinition = valueDefinitionMap.get(input.toString()); - assertNotNull(enumValueDefinition, () -> i18nMsg(locale, "Enum.badName", name, input.toString())); + if (enumValueDefinition == null) { + assertShouldNeverHappen(i18nMsg(locale, "Enum.badName", name, input.toString())); + }; return EnumValue.newEnumValue(enumValueDefinition.getName()).build(); } diff --git a/src/test/java/benchmark/AssertBenchmark.java b/src/test/java/benchmark/AssertBenchmark.java index 192b267280..04a11c03b2 100644 --- a/src/test/java/benchmark/AssertBenchmark.java +++ b/src/test/java/benchmark/AssertBenchmark.java @@ -22,7 +22,7 @@ public class AssertBenchmark { private static final int LOOPS = 100; - private static final Random random = new Random(); + private static final boolean BOOL = new Random().nextBoolean(); @Benchmark @BenchmarkMode(Mode.Throughput) @@ -62,9 +62,9 @@ public void benchMarkAssertWithStringFormatted() { } private boolean jitTrue() { - int i = random.nextInt(); // can you jit this away, Mr JIT?? - return i % 10 < -1 || i * 2 + 1 < -2; + //noinspection ConstantValue,SimplifiableConditionalExpression + return BOOL ? BOOL : !BOOL; } public static void main(String[] args) throws RunnerException { From fb1de77dca844821e498675263bbaa418177cf17 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 8 Mar 2024 09:59:56 +1100 Subject: [PATCH 300/393] Small tweaks to not allocate objects for lambda closures --- .../graphql/execution/ExecutionStepInfo.java | 7 ++-- .../graphql/execution/ExecutionStrategy.java | 35 +++++++++++++------ .../graphql/execution/ValuesResolver.java | 4 +-- .../schema/PropertyDataFetcherHelper.java | 5 +-- .../graphql/schema/PropertyFetchingImpl.java | 10 +++--- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index 08836d977f..27313bb965 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfo.java +++ b/src/main/java/graphql/execution/ExecutionStepInfo.java @@ -299,11 +299,8 @@ public Builder path(ResultPath resultPath) { return this; } - public Builder arguments(Supplier> arguments) { - this.arguments = () -> { - Map map = arguments.get(); - return map == null ? ImmutableMapWithNullValues.emptyMap() : ImmutableMapWithNullValues.copyOf(map); - }; + public Builder arguments(Supplier> arguments) { + this.arguments = arguments; return this; } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 8a58d1d9b3..1cc75af21b 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -12,7 +12,7 @@ import graphql.TrivialDataFetcher; import graphql.TypeMismatchError; import graphql.UnresolvedTypeError; -import graphql.collect.ImmutableKit; +import graphql.collect.ImmutableMapWithNullValues; import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; import graphql.execution.incremental.DeferredExecutionSupport; @@ -997,20 +997,12 @@ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionCo ExecutionStepInfo parentStepInfo = parameters.getExecutionStepInfo(); GraphQLOutputType fieldType = fieldDefinition.getType(); List fieldArgDefs = fieldDefinition.getArguments(); - Supplier> argumentValues = ImmutableKit::emptyMap; + Supplier> argumentValues = ImmutableMapWithNullValues::emptyMap; // // no need to create args at all if there are none on the field def // if (!fieldArgDefs.isEmpty()) { - List fieldArgs = field.getArguments(); - GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); - Supplier> argValuesSupplier = () -> ValuesResolver.getArgumentValues(codeRegistry, - fieldArgDefs, - fieldArgs, - executionContext.getCoercedVariables(), - executionContext.getGraphQLContext(), - executionContext.getLocale()); - argumentValues = FpKit.intraThreadMemoize(argValuesSupplier); + argumentValues = getArgumentValues(executionContext, fieldArgDefs, field.getArguments()); } @@ -1024,4 +1016,25 @@ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionCo .arguments(argumentValues) .build(); } + + @NotNull + private static Supplier> getArgumentValues(ExecutionContext executionContext, + List fieldArgDefs, + List fieldArgs) { + Supplier> argumentValues; + GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); + Supplier> argValuesSupplier = () -> { + Map resolvedValues = ValuesResolver.getArgumentValues(codeRegistry, + fieldArgDefs, + fieldArgs, + executionContext.getCoercedVariables(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); + + return ImmutableMapWithNullValues.copyOf(resolvedValues); + }; + argumentValues = FpKit.intraThreadMemoize(argValuesSupplier); + return argumentValues; + } + } diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index 38ea753f37..94073cbe6d 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -1,12 +1,10 @@ package graphql.execution; -import graphql.Assert; import graphql.GraphQLContext; import graphql.Internal; import graphql.collect.ImmutableKit; import graphql.execution.values.InputInterceptor; -import graphql.i18n.I18n; import graphql.language.Argument; import graphql.language.ArrayValue; import graphql.language.NullValue; @@ -200,6 +198,7 @@ public static Map getNormalizedArgumentValues( return result; } + @NotNull public static Map getArgumentValues( GraphQLCodeRegistry codeRegistry, List argumentTypes, @@ -319,6 +318,7 @@ public static T getInputValueImpl( } + @NotNull private static Map getArgumentValuesImpl( InputInterceptor inputInterceptor, GraphqlFieldVisibility fieldVisibility, diff --git a/src/main/java/graphql/schema/PropertyDataFetcherHelper.java b/src/main/java/graphql/schema/PropertyDataFetcherHelper.java index 2c38b5e127..0877d93688 100644 --- a/src/main/java/graphql/schema/PropertyDataFetcherHelper.java +++ b/src/main/java/graphql/schema/PropertyDataFetcherHelper.java @@ -13,13 +13,14 @@ public class PropertyDataFetcherHelper { private static final PropertyFetchingImpl impl = new PropertyFetchingImpl(DataFetchingEnvironment.class); + private static final Supplier ALWAYS_NULL = () -> null; public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType) { - return impl.getPropertyValue(propertyName, object, graphQLType, false, () -> null); + return impl.getPropertyValue(propertyName, object, graphQLType, false, ALWAYS_NULL); } public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, Supplier environment) { - return impl.getPropertyValue(propertyName, object, graphQLType, true, environment::get); + return impl.getPropertyValue(propertyName, object, graphQLType, true, environment); } public static void clearReflectionCache() { diff --git a/src/main/java/graphql/schema/PropertyFetchingImpl.java b/src/main/java/graphql/schema/PropertyFetchingImpl.java index c637ec7115..6126159cf0 100644 --- a/src/main/java/graphql/schema/PropertyFetchingImpl.java +++ b/src/main/java/graphql/schema/PropertyFetchingImpl.java @@ -66,7 +66,7 @@ private static final class CachedLambdaFunction { } } - public Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, boolean dfeInUse, Supplier singleArgumentValue) { + public Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, boolean dfeInUse, Supplier singleArgumentValue) { if (object instanceof Map) { return ((Map) object).get(propertyName); } @@ -194,12 +194,12 @@ private interface MethodFinder { Method apply(Class aClass, String s) throws NoSuchMethodException; } - private Object getPropertyViaRecordMethod(Object object, String propertyName, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { + private Object getPropertyViaRecordMethod(Object object, String propertyName, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { Method method = methodFinder.apply(object.getClass(), propertyName); return invokeMethod(object, singleArgumentValue, method, takesSingleArgumentTypeAsOnlyArgument(method)); } - private Object getPropertyViaGetterMethod(Object object, String propertyName, GraphQLType graphQLType, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { + private Object getPropertyViaGetterMethod(Object object, String propertyName, GraphQLType graphQLType, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { if (isBooleanProperty(graphQLType)) { try { return getPropertyViaGetterUsingPrefix(object, propertyName, "is", methodFinder, singleArgumentValue); @@ -211,7 +211,7 @@ private Object getPropertyViaGetterMethod(Object object, String propertyName, Gr } } - private Object getPropertyViaGetterUsingPrefix(Object object, String propertyName, String prefix, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { + private Object getPropertyViaGetterUsingPrefix(Object object, String propertyName, String prefix, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { String getterName = prefix + StringKit.capitalize(propertyName); Method method = methodFinder.apply(object.getClass(), getterName); return invokeMethod(object, singleArgumentValue, method, takesSingleArgumentTypeAsOnlyArgument(method)); @@ -340,7 +340,7 @@ private Object getPropertyViaFieldAccess(CacheKey cacheKey, Object object, Strin } } - private Object invokeMethod(Object object, Supplier singleArgumentValue, Method method, boolean takesSingleArgument) throws FastNoSuchMethodException { + private Object invokeMethod(Object object, Supplier singleArgumentValue, Method method, boolean takesSingleArgument) throws FastNoSuchMethodException { try { if (takesSingleArgument) { Object argValue = singleArgumentValue.get(); From 4912feb7818d4c71c3dd6dc6fd5be1a73b9a1fce Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 8 Mar 2024 11:07:50 +1000 Subject: [PATCH 301/393] publishing of agent --- agent/build.gradle | 86 ++++++++++++++++++++++++++++++++++++++++++++++ build.gradle | 1 + 2 files changed, 87 insertions(+) diff --git a/agent/build.gradle b/agent/build.gradle index d312c36276..af23b0c5c6 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -1,10 +1,13 @@ plugins { id 'java' + id 'java-library' + id 'maven-publish' id "com.github.johnrengelman.shadow" version "8.1.1" } dependencies { implementation("net.bytebuddy:byte-buddy:1.14.11") + // graphql-java itself implementation(rootProject) } @@ -36,3 +39,86 @@ shadowJar { ) } } + +task sourcesJar(type: Jar) { + dependsOn classes + archiveClassifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.destinationDir +} + +publishing { + + publications { + + agent(MavenPublication) { + version rootProject.version + group rootProject.group + artifactId 'graphql-java-agent' + from components.java + + artifact sourcesJar { + archiveClassifier = "sources" + } + artifact javadocJar { + archiveClassifier = "javadoc" + } + pom.withXml { + // removing the shaded dependencies from the pom + def pomNode = asNode() + pomNode.dependencies.'*'.findAll() { + it.artifactId.text() == 'graphql-java' || it.artifactId.text() == 'byte-buddy' + }.each() { + it.parent().remove(it) + } + pomNode.children().last() + { + resolveStrategy = Closure.DELEGATE_FIRST + name 'graphql-java-agent' + description 'GraphqL Java Agent' + url "https://github.com/graphql-java/graphql-java" + scm { + url "https://github.com/graphql-java/graphql-java" + connection "https://github.com/graphql-java/graphql-java" + developerConnection "https://github.com/graphql-java/graphql-java" + } + licenses { + license { + name 'MIT' + url 'https://github.com/graphql-java/graphql-java/blob/master/LICENSE.md' + distribution 'repo' + } + } + developers { + developer { + id 'andimarek' + name 'Andreas Marek' + } + } + } + } + } + } +} + +signing { + required { !project.hasProperty('publishToMavenLocal') } + def signingKey = System.env.MAVEN_CENTRAL_PGP_KEY + useInMemoryPgpKeys(signingKey, "") + sign publishing.publications +} + + +// all publish tasks depend on the build task +tasks.withType(PublishToMavenRepository) { + dependsOn build +} + +// Only publish Maven POM, disable default Gradle modules file +tasks.withType(GenerateModuleMetadata) { + enabled = false +} + diff --git a/build.gradle b/build.gradle index baeea5825f..a46ff7c393 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ def getDevelopmentVersion() { return makeDevelopmentVersion(["0.0.0", dateTime, "no-git"]) } + // a default Github Action env variable set to 'true' def isCi = Boolean.parseBoolean(System.env.CI) if (isCi) { def gitHashOutput = new StringBuilder() From da2918e14fb99b0b109b9c057354e3641a2ed4ee Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 10:19:02 +1100 Subject: [PATCH 302/393] Removed deprecated methods in parsing --- src/main/java/graphql/parser/Parser.java | 64 ++----------------- .../graphql/parser/ParserExceptionTest.groovy | 6 +- .../graphql/parser/SDLParserTest.groovy | 8 ++- 3 files changed, 15 insertions(+), 63 deletions(-) diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index a02ea9567a..15a0f5f641 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -136,81 +136,29 @@ public Document parseDocument(ParserEnvironment environment) throws InvalidSynta * @throws InvalidSyntaxException if the input is not valid graphql syntax */ public Document parseDocument(String input) throws InvalidSyntaxException { - return parseDocument(input, (ParserOptions) null); - } - - /** - * Parses reader input into a graphql AST {@link Document} - * - * @param reader the reader input to parse - * - * @return an AST {@link Document} - * - * @throws InvalidSyntaxException if the input is not valid graphql syntax - */ - public Document parseDocument(Reader reader) throws InvalidSyntaxException { - ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() - .document(reader) - .build(); - return parseDocumentImpl(parserEnvironment); - } - - /** - * Parses a string input into a graphql AST {@link Document} - * - * @param input the input to parse - * @param sourceName - the name to attribute to the input text in {@link SourceLocation#getSourceName()} - * - * @return an AST {@link Document} - * - * @throws InvalidSyntaxException if the input is not valid graphql syntax - * @deprecated use {#{@link #parse(ParserEnvironment)}} instead - */ - @Deprecated(since = "2022-08-31") - public Document parseDocument(String input, String sourceName) throws InvalidSyntaxException { MultiSourceReader multiSourceReader = MultiSourceReader.newMultiSourceReader() - .string(input, sourceName) + .string(input, null) .trackData(true) .build(); - return parseDocument(multiSourceReader); - } - /** - * Parses a string input into a graphql AST {@link Document} - * - * @param input the input to parse - * @param parserOptions the parser options - * - * @return an AST {@link Document} - * - * @throws InvalidSyntaxException if the input is not valid graphql syntax - * @deprecated use {#{@link #parse(ParserEnvironment)}} instead - */ - @Deprecated(since = "2022-08-31") - public Document parseDocument(String input, ParserOptions parserOptions) throws InvalidSyntaxException { - MultiSourceReader multiSourceReader = MultiSourceReader.newMultiSourceReader() - .string(input, null) - .trackData(true) + ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() + .document(multiSourceReader) .build(); - return parseDocument(multiSourceReader, parserOptions); + return parseDocumentImpl(parserEnvironment); } /** * Parses reader input into a graphql AST {@link Document} * - * @param reader the reader input to parse - * @param parserOptions the parser options + * @param reader the reader input to parse * * @return an AST {@link Document} * * @throws InvalidSyntaxException if the input is not valid graphql syntax - * @deprecated use {#{@link #parse(ParserEnvironment)}} instead */ - @Deprecated(since = "2022-08-31") - public Document parseDocument(Reader reader, ParserOptions parserOptions) throws InvalidSyntaxException { + public Document parseDocument(Reader reader) throws InvalidSyntaxException { ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() .document(reader) - .parserOptions(parserOptions) .build(); return parseDocumentImpl(parserEnvironment); } diff --git a/src/test/groovy/graphql/parser/ParserExceptionTest.groovy b/src/test/groovy/graphql/parser/ParserExceptionTest.groovy index 51ebe9f32b..459ba957b6 100644 --- a/src/test/groovy/graphql/parser/ParserExceptionTest.groovy +++ b/src/test/groovy/graphql/parser/ParserExceptionTest.groovy @@ -79,10 +79,12 @@ fragment X on SomeType { } ''' when: - new Parser().parseDocument(sdl, "namedSource") + Reader reader = MultiSourceReader.newMultiSourceReader() + .string(sdl, "namedSource") + .build() + new Parser().parseDocument(reader) then: def e = thrown(InvalidSyntaxException) - print e e.location.line == 2 e.location.column == 13 diff --git a/src/test/groovy/graphql/parser/SDLParserTest.groovy b/src/test/groovy/graphql/parser/SDLParserTest.groovy index e2973ccabd..928619cf23 100644 --- a/src/test/groovy/graphql/parser/SDLParserTest.groovy +++ b/src/test/groovy/graphql/parser/SDLParserTest.groovy @@ -800,13 +800,15 @@ input Gun { when: def defaultDoc = new Parser().parseDocument(input) - def namedDocNull = new Parser().parseDocument(input, (String) null) - def namedDoc = new Parser().parseDocument(input, sourceName) + Reader reader = MultiSourceReader.newMultiSourceReader() + .string(input, sourceName) + .build(); + + def namedDoc = new Parser().parseDocument(reader) then: defaultDoc.definitions[0].sourceLocation.sourceName == null - namedDocNull.definitions[0].sourceLocation.sourceName == null namedDoc.definitions[0].sourceLocation.sourceName == sourceName } From 0b957bbd6a4b2a8e893154b2c277f92a593565dc Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 10:35:19 +1100 Subject: [PATCH 303/393] Removed deprecated methods in doc providers --- .../NoOpPreparsedDocumentProvider.java | 5 ++-- .../preparsed/PreparsedDocumentProvider.java | 21 +------------ .../InMemoryPersistedQueryCache.java | 6 ++-- .../persisted/PersistedQueryCache.java | 27 +---------------- .../persisted/PersistedQuerySupport.java | 10 ++++--- .../NoOpPreparsedDocumentProviderTest.groovy | 4 +-- .../PreparsedDocumentProviderTest.groovy | 30 ++----------------- .../TestingPreparsedDocumentProvider.groovy | 5 ++-- .../ApolloPersistedQuerySupportTest.groovy | 25 ++++++++-------- .../InMemoryPersistedQueryCacheTest.groovy | 4 +-- 10 files changed, 38 insertions(+), 99 deletions(-) diff --git a/src/main/java/graphql/execution/preparsed/NoOpPreparsedDocumentProvider.java b/src/main/java/graphql/execution/preparsed/NoOpPreparsedDocumentProvider.java index 912b850db0..03a96776b6 100644 --- a/src/main/java/graphql/execution/preparsed/NoOpPreparsedDocumentProvider.java +++ b/src/main/java/graphql/execution/preparsed/NoOpPreparsedDocumentProvider.java @@ -4,6 +4,7 @@ import graphql.ExecutionInput; import graphql.Internal; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; @Internal @@ -11,7 +12,7 @@ public class NoOpPreparsedDocumentProvider implements PreparsedDocumentProvider public static final NoOpPreparsedDocumentProvider INSTANCE = new NoOpPreparsedDocumentProvider(); @Override - public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction) { - return parseAndValidateFunction.apply(executionInput); + public CompletableFuture getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction) { + return CompletableFuture.completedFuture(parseAndValidateFunction.apply(executionInput)); } } diff --git a/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java b/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java index 402b401e43..7aac05d09d 100644 --- a/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java +++ b/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java @@ -12,23 +12,6 @@ */ @PublicSpi public interface PreparsedDocumentProvider { - /** - * This is called to get a "cached" pre-parsed query and if it's not present, then the "parseAndValidateFunction" - * can be called to parse and validate the query. - *

- * Note - the "parseAndValidateFunction" MUST be called if you don't 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 don't 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} - *

- * @deprecated - use {@link #getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction)} - */ - @Deprecated(since = "2021-12-06") - PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction); - /** * This is called to get a "cached" pre-parsed query and if it's not present, then the "parseAndValidateFunction" * can be called to parse and validate the query. @@ -41,9 +24,7 @@ public interface PreparsedDocumentProvider { * @param parseAndValidateFunction If the query has not be pre-parsed, this function MUST be called to parse and validate it * @return a promise to an {@link PreparsedDocumentEntry} */ - default CompletableFuture getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction) { - return CompletableFuture.completedFuture(getDocument(executionInput, parseAndValidateFunction)); - } + CompletableFuture getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction); } diff --git a/src/main/java/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCache.java b/src/main/java/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCache.java index d8f6dd458c..5226332b85 100644 --- a/src/main/java/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCache.java +++ b/src/main/java/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCache.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; /** @@ -27,8 +28,8 @@ public Map getKnownQueries() { } @Override - public PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound { - return cache.compute(persistedQueryId, (k, v) -> { + public CompletableFuture getPersistedQueryDocumentAsync(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound { + PreparsedDocumentEntry documentEntry = cache.compute(persistedQueryId, (k, v) -> { if (v != null) { return v; } @@ -45,6 +46,7 @@ public PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, } return onCacheMiss.apply(queryText); }); + return CompletableFuture.completedFuture(documentEntry); } public static Builder newInMemoryPersistedQueryCache() { diff --git a/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java b/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java index 52d1e79a18..7690280e78 100644 --- a/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java +++ b/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java @@ -11,29 +11,6 @@ */ @PublicSpi public interface PersistedQueryCache { - - /** - * This is called to get a persisted query from cache. - *

- * 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. - *

- * If it's 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 PreparsedDocumentEntry will be passed back ready for you to cache it. - *

- * If it's 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 it's not currently 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 - * - * @deprecated - use {@link #getPersistedQueryDocumentAsync(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss)} - */ - @Deprecated(since = "2021-12-06") - PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound; - /** * This is called to get a persisted query from cache. *

@@ -51,7 +28,5 @@ public interface PersistedQueryCache { * @return a promise to parsed and validated {@link 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 */ - default CompletableFuture getPersistedQueryDocumentAsync(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound{ - return CompletableFuture.completedFuture(getPersistedQueryDocument(persistedQueryId, executionInput, onCacheMiss)); - } + CompletableFuture getPersistedQueryDocumentAsync(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound; } diff --git a/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java b/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java index af2edef379..849e10c510 100644 --- a/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java +++ b/src/main/java/graphql/execution/preparsed/persisted/PersistedQuerySupport.java @@ -8,9 +8,11 @@ import graphql.execution.preparsed.PreparsedDocumentProvider; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import static graphql.Assert.assertNotNull; +import static java.util.concurrent.CompletableFuture.completedFuture; /** * This abstract class forms the basis for persistent query support. Derived classes @@ -36,14 +38,14 @@ public PersistedQuerySupport(PersistedQueryCache persistedQueryCache) { } @Override - public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction) { + public CompletableFuture getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction) { Optional queryIdOption = getPersistedQueryId(executionInput); assertNotNull(queryIdOption, () -> String.format("The class %s MUST return a non null optional query id", this.getClass().getName())); try { if (queryIdOption.isPresent()) { Object persistedQueryId = queryIdOption.get(); - return persistedQueryCache.getPersistedQueryDocument(persistedQueryId, executionInput, (queryText) -> { + return persistedQueryCache.getPersistedQueryDocumentAsync(persistedQueryId, executionInput, (queryText) -> { // we have a miss and they gave us nothing - bah! if (queryText == null || queryText.isBlank()) { throw new PersistedQueryNotFound(persistedQueryId); @@ -57,9 +59,9 @@ public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Functio }); } // ok there is no query id - we assume the query is indeed ready to go as is - ie its not a persisted query - return parseAndValidateFunction.apply(executionInput); + return completedFuture(parseAndValidateFunction.apply(executionInput)); } catch (PersistedQueryError e) { - return mkMissingError(e); + return completedFuture(mkMissingError(e)); } } diff --git a/src/test/groovy/graphql/execution/preparsed/NoOpPreparsedDocumentProviderTest.groovy b/src/test/groovy/graphql/execution/preparsed/NoOpPreparsedDocumentProviderTest.groovy index d0c38760e1..1926f0ff77 100644 --- a/src/test/groovy/graphql/execution/preparsed/NoOpPreparsedDocumentProviderTest.groovy +++ b/src/test/groovy/graphql/execution/preparsed/NoOpPreparsedDocumentProviderTest.groovy @@ -13,10 +13,10 @@ class NoOpPreparsedDocumentProviderTest extends Specification { def documentEntry = new PreparsedDocumentEntry(Document.newDocument().build()) when: - def actual = provider.getDocument(newExecutionInput("{}").build(), { return documentEntry }) + def actual = provider.getDocumentAsync(newExecutionInput("{}").build(), { return documentEntry }) then: - actual == documentEntry + actual.join() == documentEntry } } diff --git a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy index 330ce02dbc..fc1eb054f1 100644 --- a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy +++ b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy @@ -11,9 +11,9 @@ import graphql.execution.instrumentation.LegacyTestingInstrumentation import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.language.Document -import graphql.parser.Parser import spock.lang.Specification +import java.util.concurrent.CompletableFuture import java.util.function.Function import static graphql.ExecutionInput.newExecutionInput @@ -193,13 +193,13 @@ class PreparsedDocumentProviderTest extends Specification { def documentProvider = new PreparsedDocumentProvider() { @Override - PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction) { + CompletableFuture getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction) { if (executionInput.getQuery() == "#A") { executionInput = executionInput.transform({ it.query(queryA) }) } else { executionInput = executionInput.transform({ it.query(queryB) }) } - return parseAndValidateFunction.apply(executionInput) + return CompletableFuture.completedFuture(parseAndValidateFunction.apply(executionInput)) } } @@ -225,28 +225,4 @@ class PreparsedDocumentProviderTest extends Specification { resultB.data == [hero: [name: "R2-D2"]] instrumentationB.capturedInput.getQuery() == queryB } - - def "sync method and async method result is same"() { - given: - def provider = new TestingPreparsedDocumentProvider() - def queryA = """ - query A { - hero { - id - } - } - """ - def engineParser = { - ExecutionInput ei -> - def doc = new Parser().parseDocument(ei.getQuery()) - return new PreparsedDocumentEntry(doc) - } - when: - def syncMethod = provider.getDocument(newExecutionInput(queryA).build(), engineParser) - def asyncMethod = provider.getDocumentAsync(newExecutionInput(queryA).build(), engineParser) - - then: - asyncMethod != null - asyncMethod.get().equals(syncMethod) - } } diff --git a/src/test/groovy/graphql/execution/preparsed/TestingPreparsedDocumentProvider.groovy b/src/test/groovy/graphql/execution/preparsed/TestingPreparsedDocumentProvider.groovy index 17f4cf341e..30ae3d4f63 100644 --- a/src/test/groovy/graphql/execution/preparsed/TestingPreparsedDocumentProvider.groovy +++ b/src/test/groovy/graphql/execution/preparsed/TestingPreparsedDocumentProvider.groovy @@ -2,6 +2,7 @@ package graphql.execution.preparsed import graphql.ExecutionInput +import java.util.concurrent.CompletableFuture import java.util.function.Function @@ -9,9 +10,9 @@ class TestingPreparsedDocumentProvider implements PreparsedDocumentProvider { private Map cache = new HashMap<>() @Override - PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction) { + CompletableFuture getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction) { Function mapCompute = { key -> parseAndValidateFunction.apply(executionInput) } - return cache.computeIfAbsent(executionInput.query, mapCompute) + return CompletableFuture.completedFuture(cache.computeIfAbsent(executionInput.query, mapCompute)) } } diff --git a/src/test/groovy/graphql/execution/preparsed/persisted/ApolloPersistedQuerySupportTest.groovy b/src/test/groovy/graphql/execution/preparsed/persisted/ApolloPersistedQuerySupportTest.groovy index b9f4d49883..7b4a4cc168 100644 --- a/src/test/groovy/graphql/execution/preparsed/persisted/ApolloPersistedQuerySupportTest.groovy +++ b/src/test/groovy/graphql/execution/preparsed/persisted/ApolloPersistedQuerySupportTest.groovy @@ -5,6 +5,7 @@ import graphql.execution.preparsed.PreparsedDocumentEntry import graphql.parser.Parser import spock.lang.Specification +import java.util.concurrent.CompletableFuture import java.util.function.Function import static graphql.execution.preparsed.persisted.PersistedQuerySupport.PERSISTED_QUERY_MARKER @@ -29,11 +30,11 @@ class ApolloPersistedQuerySupportTest extends Specification { def parseCount = [:] @Override - PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound { + CompletableFuture getPersistedQueryDocumentAsync(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound { keyCount.compute(persistedQueryId, { k, v -> v == null ? 1 : v + 1 }) PreparsedDocumentEntry entry = map.get(persistedQueryId) as PreparsedDocumentEntry if (entry != null) { - return entry + return CompletableFuture.completedFuture(entry) } parseCount.compute(persistedQueryId, { k, v -> v == null ? 1 : v + 1 }) @@ -44,7 +45,7 @@ class ApolloPersistedQuerySupportTest extends Specification { } def newDocEntry = onCacheMiss.apply(queryText) map.put(persistedQueryId, newDocEntry) - return newDocEntry + return CompletableFuture.completedFuture(newDocEntry) } } @@ -66,7 +67,7 @@ class ApolloPersistedQuerySupportTest extends Specification { when: def ei = mkEI(hashOne, PERSISTED_QUERY_MARKER) - def documentEntry = apolloSupport.getDocument(ei, engineParser) + def documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() def doc = documentEntry.getDocument() then: printAstCompact(doc) == "{oneTwoThree}" @@ -75,7 +76,7 @@ class ApolloPersistedQuerySupportTest extends Specification { when: ei = mkEI(hashOne, PERSISTED_QUERY_MARKER) - documentEntry = apolloSupport.getDocument(ei, engineParser) + documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() doc = documentEntry.getDocument() then: @@ -91,7 +92,7 @@ class ApolloPersistedQuerySupportTest extends Specification { when: def ei = ExecutionInput.newExecutionInput("query { normal }").build() - def documentEntry = apolloSupport.getDocument(ei, engineParser) + def documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() def doc = documentEntry.getDocument() then: printAstCompact(doc) == "{normal}" @@ -105,7 +106,7 @@ class ApolloPersistedQuerySupportTest extends Specification { when: def ei = mkEI(hashOne, "{normal}") - def documentEntry = apolloSupport.getDocument(ei, engineParser) + def documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() def doc = documentEntry.getDocument() then: printAstCompact(doc) == "{oneTwoThree}" @@ -121,7 +122,7 @@ class ApolloPersistedQuerySupportTest extends Specification { when: def ei = mkEI("nonExistedHash", PERSISTED_QUERY_MARKER) - def documentEntry = apolloSupport.getDocument(ei, engineParser) + def documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() then: documentEntry.getDocument() == null def gqlError = documentEntry.getErrors()[0] @@ -137,21 +138,21 @@ class ApolloPersistedQuerySupportTest extends Specification { when: def ei = mkEI(hashOne, PERSISTED_QUERY_MARKER) - def documentEntry = apolloSupport.getDocument(ei, engineParser) + def documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() def doc = documentEntry.getDocument() then: printAstCompact(doc) == "{oneTwoThree}" when: ei = mkEI(hashTwo, PERSISTED_QUERY_MARKER) - documentEntry = apolloSupport.getDocument(ei, engineParser) + documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() doc = documentEntry.getDocument() then: printAstCompact(doc) == "{fourFiveSix}" when: ei = mkEI("nonExistent", PERSISTED_QUERY_MARKER) - documentEntry = apolloSupport.getDocument(ei, engineParser) + documentEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() then: documentEntry.hasErrors() } @@ -161,7 +162,7 @@ class ApolloPersistedQuerySupportTest extends Specification { def apolloSupport = new ApolloPersistedQuerySupport(cache) when: def ei = mkEI("badHash", PERSISTED_QUERY_MARKER) - def docEntry = apolloSupport.getDocument(ei, engineParser) + def docEntry = apolloSupport.getDocumentAsync(ei, engineParser).join() then: docEntry.getDocument() == null def error = docEntry.getErrors()[0] diff --git a/src/test/groovy/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCacheTest.groovy b/src/test/groovy/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCacheTest.groovy index 88b9803de5..bffc4e7d9c 100644 --- a/src/test/groovy/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCacheTest.groovy +++ b/src/test/groovy/graphql/execution/preparsed/persisted/InMemoryPersistedQueryCacheTest.groovy @@ -37,7 +37,7 @@ class InMemoryPersistedQueryCacheTest extends Specification { def ei = mkEI(hash, "query { oneTwoThreeFour }") when: - def getDoc = inMemCache.getPersistedQueryDocument(hash, ei, onMiss) + def getDoc = inMemCache.getPersistedQueryDocumentAsync(hash, ei, onMiss).join() def doc = getDoc.document then: printAstCompact(doc) == "{oneTwoThreeFour}" @@ -50,7 +50,7 @@ class InMemoryPersistedQueryCacheTest extends Specification { .build() def ei = mkEI(hash, PersistedQuerySupport.PERSISTED_QUERY_MARKER) when: - def getDoc = inMemCache.getPersistedQueryDocument(hash, ei, onMiss) + def getDoc = inMemCache.getPersistedQueryDocumentAsync(hash, ei, onMiss).join() def doc = getDoc.document then: printAstCompact(doc) == "{foo bar baz}" From 0af97b76f265207a73b1611400c428edc7aca968 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 10:50:22 +1100 Subject: [PATCH 304/393] Removed deprecated methods in ExecutionInput --- src/main/java/graphql/ExecutionInput.java | 33 +------------------ .../groovy/graphql/ExecutionInputTest.groovy | 17 ---------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index 7a4365717d..18456034ca 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -278,38 +278,7 @@ public Builder context(Object context) { return this; } - /** - * The legacy context object - * - * @param contextBuilder the context builder object to use - * - * @return this builder - * - * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now - */ - @Deprecated(since = "2021-07-05") - public Builder context(GraphQLContext.Builder contextBuilder) { - this.context = contextBuilder.build(); - return this; - } - - /** - * The legacy context object - * - * @param contextBuilderFunction the context builder function to use - * - * @return this builder - * - * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now - */ - @Deprecated(since = "2021-07-05") - public Builder context(UnaryOperator contextBuilderFunction) { - GraphQLContext.Builder builder = GraphQLContext.newContext(); - builder = contextBuilderFunction.apply(builder); - return context(builder.build()); - } - - /** + /** * This will give you a builder of {@link GraphQLContext} and any values you set will be copied * into the underlying {@link GraphQLContext} of this execution input * diff --git a/src/test/groovy/graphql/ExecutionInputTest.groovy b/src/test/groovy/graphql/ExecutionInputTest.groovy index f0b4b232fc..b3243e0d26 100644 --- a/src/test/groovy/graphql/ExecutionInputTest.groovy +++ b/src/test/groovy/graphql/ExecutionInputTest.groovy @@ -45,23 +45,6 @@ class ExecutionInputTest extends Specification { executionInput.graphQLContext.get("a") == "b" } - def "legacy context methods work"() { - // Retaining deprecated method tests for coverage - when: - def executionInput = ExecutionInput.newExecutionInput().query(query) - .context({ builder -> builder.of("k1", "v1") } as UnaryOperator) // Retain deprecated for test coverage - .build() - then: - (executionInput.context as GraphQLContext).get("k1") == "v1" // Retain deprecated for test coverage - - when: - executionInput = ExecutionInput.newExecutionInput().query(query) - .context(GraphQLContext.newContext().of("k2", "v2")) // Retain deprecated for test coverage - .build() - then: - (executionInput.context as GraphQLContext).get("k2") == "v2" // Retain deprecated for test coverage - } - def "legacy context is defaulted"() { // Retaining deprecated method tests for coverage when: From 62118c6a09a298f578004d3ee303b2d5b9857192 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 11:16:09 +1100 Subject: [PATCH 305/393] Removed deprecated methods in ExecutionContext --- src/main/java/graphql/execution/ExecutionContext.java | 10 ---------- .../graphql/schema/DataFetchingEnvironmentImpl.java | 2 +- .../execution/ExecutionContextBuilderTest.groovy | 2 +- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 71f6c1827b..fac002a060 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -120,16 +120,6 @@ public OperationDefinition getOperationDefinition() { return operationDefinition; } - /** - * @return map of coerced variables - * - * @deprecated use {@link #getCoercedVariables()} instead - */ - @Deprecated(since = "2022-05-24") - public Map getVariables() { - return coercedVariables.toMap(); - } - public CoercedVariables getCoercedVariables() { return coercedVariables; } diff --git a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java index d94197141e..4377330e48 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java @@ -94,7 +94,7 @@ public static Builder newDataFetchingEnvironment(ExecutionContext executionConte .locale(executionContext.getLocale()) .document(executionContext.getDocument()) .operationDefinition(executionContext.getOperationDefinition()) - .variables(executionContext.getVariables()) + .variables(executionContext.getCoercedVariables().toMap()) .executionId(executionContext.getExecutionId()); } diff --git a/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy b/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy index eb35f277b7..210c442989 100644 --- a/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy @@ -54,7 +54,7 @@ class ExecutionContextBuilderTest extends Specification { executionContext.root == root executionContext.context == context // Retain deprecated method for test coverage executionContext.graphQLContext == graphQLContext - executionContext.variables == [var: 'value'] // Retain deprecated method for test coverage + executionContext.getCoercedVariables().toMap() == [var: 'value'] executionContext.getFragmentsByName() == [MyFragment: fragment] executionContext.operationDefinition == operation executionContext.dataLoaderRegistry == dataLoaderRegistry From 39b92286e21ce144b81ba7b6335169de05b94949 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 17:54:03 +1100 Subject: [PATCH 306/393] Removed deprecated methods in GraphQLCodeRegistry --- .../graphql/schema/GraphQLCodeRegistry.java | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/main/java/graphql/schema/GraphQLCodeRegistry.java b/src/main/java/graphql/schema/GraphQLCodeRegistry.java index d1b0f94d4c..93b5c6306f 100644 --- a/src/main/java/graphql/schema/GraphQLCodeRegistry.java +++ b/src/main/java/graphql/schema/GraphQLCodeRegistry.java @@ -49,23 +49,6 @@ public GraphqlFieldVisibility getFieldVisibility() { return fieldVisibility; } - /** - * Returns a data fetcher associated with a field within a container type - * - * @param parentType the container type - * @param fieldDefinition the field definition - * - * @return the DataFetcher associated with this field. All fields have data fetchers - * - * @see #getDataFetcher(GraphQLObjectType, GraphQLFieldDefinition) - * @deprecated This is confusing because {@link GraphQLInterfaceType}s cant have data fetchers. At runtime only a {@link GraphQLObjectType} - * can be used to fetch a field. This method allows the mapping to be made, but it is never useful if an interface is passed in. - */ - @Deprecated(since = "2023-05-13") - public DataFetcher getDataFetcher(GraphQLFieldsContainer parentType, GraphQLFieldDefinition fieldDefinition) { - return getDataFetcherImpl(FieldCoordinates.coordinates(parentType, fieldDefinition), fieldDefinition, dataFetcherMap, systemDataFetcherMap, defaultDataFetcherFactory); - } - /** * Returns a data fetcher associated with a field within an object type * @@ -252,23 +235,6 @@ private Builder markChanged(boolean condition) { return this; } - /** - * Returns a data fetcher associated with a field within a container type - * - * @param parentType the container type - * @param fieldDefinition the field definition - * - * @return the DataFetcher associated with this field. All fields have data fetchers - * - * @see #getDataFetcher(GraphQLObjectType, GraphQLFieldDefinition) - * @deprecated This is confusing because {@link GraphQLInterfaceType}s cant have data fetchers. At runtime only a {@link GraphQLObjectType} - * can be used to fetch a field. This method allows the mapping to be made, but it is never useful if an interface is passed in. - */ - @Deprecated(since = "2023-05-13") - public DataFetcher getDataFetcher(GraphQLFieldsContainer parentType, GraphQLFieldDefinition fieldDefinition) { - return getDataFetcherImpl(FieldCoordinates.coordinates(parentType, fieldDefinition), fieldDefinition, dataFetcherMap, systemDataFetcherMap, defaultDataFetcherFactory); - } - /** * Returns a data fetcher associated with a field within an object type * @@ -357,24 +323,6 @@ public Builder dataFetcher(FieldCoordinates coordinates, DataFetcher dataFetc return dataFetcher(assertNotNull(coordinates), DataFetcherFactories.useDataFetcher(dataFetcher)); } - /** - * Sets the data fetcher for a specific field inside a container type - * - * @param parentType the container type - * @param fieldDefinition the field definition - * @param dataFetcher the data fetcher code for that field - * - * @return this builder - * - * @see #dataFetcher(GraphQLObjectType, GraphQLFieldDefinition, DataFetcher) - * @deprecated This is confusing because {@link GraphQLInterfaceType}s cant have data fetchers. At runtime only a {@link GraphQLObjectType} - * can be used to fetch a field. This method allows the mapping to be made, but it is never useful if an interface is passed in. - */ - @Deprecated(since = "2023-05-13") - public Builder dataFetcher(GraphQLFieldsContainer parentType, GraphQLFieldDefinition fieldDefinition, DataFetcher dataFetcher) { - return dataFetcher(FieldCoordinates.coordinates(parentType.getName(), fieldDefinition.getName()), dataFetcher); - } - /** * Sets the data fetcher for a specific field inside an object type * From a0663c4795381ddffad35b54c27bce3497b24f52 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 18:48:47 +1100 Subject: [PATCH 307/393] Removed deprecated methods in GraphQLSchema --- .../java/graphql/schema/GraphQLSchema.java | 56 +------------------ .../graphql/validation/TraversalContext.java | 4 +- .../GraphqlFieldVisibilityTest.groovy | 31 ---------- 3 files changed, 3 insertions(+), 88 deletions(-) diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index e7026ad6c3..c36df39b36 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -18,7 +18,6 @@ import graphql.schema.validation.InvalidSchemaException; import graphql.schema.validation.SchemaValidationError; import graphql.schema.validation.SchemaValidator; -import graphql.schema.visibility.GraphqlFieldVisibility; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -412,16 +411,6 @@ public GraphQLObjectType getSubscriptionType() { return subscriptionType; } - /** - * @return the field visibility - * - * @deprecated use {@link GraphQLCodeRegistry#getFieldVisibility()} instead - */ - @Deprecated(since = "2018-12-03") - public GraphqlFieldVisibility getFieldVisibility() { - return codeRegistry.getFieldVisibility(); - } - /** * This returns the list of directives definitions that are associated with this schema object including * built in ones. @@ -451,7 +440,6 @@ public GraphQLDirective getDirective(String directiveName) { } - /** * This returns the list of directives that have been explicitly applied to the * schema object. Note that {@link #getDirectives()} will return @@ -729,19 +717,6 @@ public Builder subscription(GraphQLObjectType subscriptionType) { return this; } - /** - * @param fieldVisibility the field visibility - * - * @return this builder - * - * @deprecated use {@link graphql.schema.GraphQLCodeRegistry.Builder#fieldVisibility(graphql.schema.visibility.GraphqlFieldVisibility)} instead - */ - @Deprecated(since = "2018-12-03") - public Builder fieldVisibility(GraphqlFieldVisibility fieldVisibility) { - this.codeRegistry = this.codeRegistry.transform(builder -> builder.fieldVisibility(fieldVisibility)); - return this; - } - public Builder codeRegistry(GraphQLCodeRegistry codeRegistry) { this.codeRegistry = codeRegistry; return this; @@ -857,35 +832,6 @@ public Builder introspectionSchemaType(GraphQLObjectType introspectionSchemaType return this; } - /** - * Builds the schema - * - * @param additionalTypes - please don't use this anymore - * - * @return the built schema - * - * @deprecated - Use the {@link #additionalType(GraphQLType)} methods - */ - @Deprecated(since = "2018-07-30") - public GraphQLSchema build(Set additionalTypes) { - return additionalTypes(additionalTypes).build(); - } - - /** - * Builds the schema - * - * @param additionalTypes - please don't use this anymore - * @param additionalDirectives - please don't use this anymore - * - * @return the built schema - * - * @deprecated - Use the {@link #additionalType(GraphQLType)} and {@link #additionalDirective(GraphQLDirective)} methods - */ - @Deprecated(since = "2018-07-30") - public GraphQLSchema build(Set additionalTypes, Set additionalDirectives) { - return additionalTypes(additionalTypes).additionalDirectives(additionalDirectives).build(); - } - /** * Builds the schema * @@ -935,7 +881,7 @@ private GraphQLSchema buildImpl() { private GraphQLSchema validateSchema(GraphQLSchema graphQLSchema) { Collection errors = new SchemaValidator().validateSchema(graphQLSchema); - if (errors.size() > 0) { + if (!errors.isEmpty()) { throw new InvalidSchemaException(errors); } return graphQLSchema; diff --git a/src/main/java/graphql/validation/TraversalContext.java b/src/main/java/graphql/validation/TraversalContext.java index 93bbdb62e6..d9d93af7f1 100644 --- a/src/main/java/graphql/validation/TraversalContext.java +++ b/src/main/java/graphql/validation/TraversalContext.java @@ -178,7 +178,7 @@ private void enterImpl(ObjectField objectField) { GraphQLInputObjectField inputField = null; if (objectType instanceof GraphQLInputObjectType) { GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) objectType; - inputField = schema.getFieldVisibility().getFieldDefinition(inputObjectType, objectField.getName()); + inputField = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(inputObjectType, objectField.getName()); if (inputField != null) { inputType = inputField.getType(); } @@ -337,7 +337,7 @@ private GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLType par return schema.getIntrospectionTypenameFieldDefinition(); } if (parentType instanceof GraphQLFieldsContainer) { - return schema.getFieldVisibility().getFieldDefinition((GraphQLFieldsContainer) parentType, field.getName()); + return schema.getCodeRegistry().getFieldVisibility().getFieldDefinition((GraphQLFieldsContainer) parentType, field.getName()); } return null; } diff --git a/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy b/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy index 9f3e083977..e292cffb9a 100644 --- a/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy +++ b/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy @@ -26,37 +26,6 @@ import static graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility.NO class GraphqlFieldVisibilityTest extends Specification { - def "visibility is enforced"() { - GraphqlFieldVisibility banNameVisibility = newBlock().addPattern(".*\\.name").build() - def schema = GraphQLSchema.newSchema() - .query(StarWarsSchema.queryType) - .codeRegistry(StarWarsSchema.codeRegistry) - .fieldVisibility(banNameVisibility) // Retain deprecated builder for test coverage - .build() - - def graphQL = GraphQL.newGraphQL(schema).build() - - given: - def query = """ - { - hero { - id - name - friends { - aliasHandled: name - } - } - } - """ - - when: - def result = graphQL.execute(query) - - then: - result.errors[0].getMessage().contains("Field 'name' in type 'Character' is undefined") - result.errors[1].getMessage().contains("Field 'name' in type 'Character' is undefined") - } - def "introspection visibility is enforced"() { given: GraphQLCodeRegistry codeRegistry = StarWarsSchema.codeRegistry.transform(builder -> { From a2c9cf3cc9af16b682d0fe7437a0fd5fca308ed6 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 19:09:50 +1100 Subject: [PATCH 308/393] Removed deprecated methods and SchemaGeneratorPostProcessing --- .../graphql/schema/idl/RuntimeWiring.java | 22 ---------------- .../graphql/schema/idl/SchemaGenerator.java | 7 ----- .../idl/SchemaGeneratorPostProcessing.java | 25 ------------------ .../schema/idl/SchemaGeneratorTest.groovy | 26 ------------------- 4 files changed, 80 deletions(-) delete mode 100644 src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java diff --git a/src/main/java/graphql/schema/idl/RuntimeWiring.java b/src/main/java/graphql/schema/idl/RuntimeWiring.java index 52f909ac63..9edc34a246 100644 --- a/src/main/java/graphql/schema/idl/RuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/RuntimeWiring.java @@ -35,7 +35,6 @@ public class RuntimeWiring { private final List directiveWiring; private final WiringFactory wiringFactory; private final Map enumValuesProviders; - private final Collection schemaGeneratorPostProcessings; private final GraphqlFieldVisibility fieldVisibility; private final GraphQLCodeRegistry codeRegistry; private final GraphqlTypeComparatorRegistry comparatorRegistry; @@ -57,7 +56,6 @@ private RuntimeWiring(Builder builder) { this.directiveWiring = builder.directiveWiring; this.wiringFactory = builder.wiringFactory; this.enumValuesProviders = builder.enumValuesProviders; - this.schemaGeneratorPostProcessings = builder.schemaGeneratorPostProcessings; this.fieldVisibility = builder.fieldVisibility; this.codeRegistry = builder.codeRegistry; this.comparatorRegistry = builder.comparatorRegistry; @@ -85,7 +83,6 @@ public static Builder newRuntimeWiring(RuntimeWiring originalRuntimeWiring) { builder.directiveWiring.addAll(originalRuntimeWiring.directiveWiring); builder.wiringFactory = originalRuntimeWiring.wiringFactory; builder.enumValuesProviders.putAll(originalRuntimeWiring.enumValuesProviders); - builder.schemaGeneratorPostProcessings.addAll(originalRuntimeWiring.schemaGeneratorPostProcessings); builder.fieldVisibility = originalRuntimeWiring.fieldVisibility; builder.codeRegistry = originalRuntimeWiring.codeRegistry; builder.comparatorRegistry = originalRuntimeWiring.comparatorRegistry; @@ -150,10 +147,6 @@ public List getDirectiveWiring() { return directiveWiring; } - public Collection getSchemaGeneratorPostProcessings() { - return schemaGeneratorPostProcessings; - } - public GraphqlTypeComparatorRegistry getComparatorRegistry() { return comparatorRegistry; } @@ -167,7 +160,6 @@ public static class Builder { private final Map enumValuesProviders = new LinkedHashMap<>(); private final Map registeredDirectiveWiring = new LinkedHashMap<>(); private final List directiveWiring = new ArrayList<>(); - private final Collection schemaGeneratorPostProcessings = new ArrayList<>(); private WiringFactory wiringFactory = new NoopWiringFactory(); private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY; private GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry().build(); @@ -346,20 +338,6 @@ public Builder comparatorRegistry(GraphqlTypeComparatorRegistry comparatorRegist return this; } - /** - * Adds a schema transformer into the mix - * - * @param schemaGeneratorPostProcessing the non null schema transformer to add - * - * @return the runtime wiring builder - * @deprecated This mechanism can be achieved in a better way via {@link graphql.schema.SchemaTransformer} - * after the schema is built - */ - @Deprecated(since = "2022-10-29") - public Builder transformer(SchemaGeneratorPostProcessing schemaGeneratorPostProcessing) { - this.schemaGeneratorPostProcessings.add(assertNotNull(schemaGeneratorPostProcessing)); - return this; - } /** * @return the built runtime wiring diff --git a/src/main/java/graphql/schema/idl/SchemaGenerator.java b/src/main/java/graphql/schema/idl/SchemaGenerator.java index 819f820734..868aec4b76 100644 --- a/src/main/java/graphql/schema/idl/SchemaGenerator.java +++ b/src/main/java/graphql/schema/idl/SchemaGenerator.java @@ -154,13 +154,6 @@ private GraphQLSchema makeExecutableSchemaImpl(TypeDefinitionRegistry typeRegist buildCtx.getCodeRegistry()); graphQLSchema = directiveWiringProcessing.process(graphQLSchema); } - - // - // SchemaGeneratorPostProcessing is deprecated but for now we continue to run them - // - for (SchemaGeneratorPostProcessing postProcessing : buildCtx.getWiring().getSchemaGeneratorPostProcessings()) { - graphQLSchema = postProcessing.process(graphQLSchema); - } return graphQLSchema; } diff --git a/src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java b/src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java deleted file mode 100644 index a0bdd91049..0000000000 --- a/src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java +++ /dev/null @@ -1,25 +0,0 @@ -package graphql.schema.idl; - -import graphql.PublicSpi; -import graphql.schema.GraphQLSchema; - -/** - * These are called by the {@link SchemaGenerator} after a valid schema has been built - * and they can then adjust it accordingly with some sort of post processing. - * - * @deprecated This mechanism can be achieved in a better way via {@link graphql.schema.SchemaTransformer} - * after the schema is built - */ -@PublicSpi -@Deprecated(since = "2022-10-29") -public interface SchemaGeneratorPostProcessing { - - /** - * Called to transform the schema from its built state into something else - * - * @param originalSchema the original built schema - * - * @return a non null schema - */ - GraphQLSchema process(GraphQLSchema originalSchema); -} diff --git a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy index 0d2be323d0..e24317e6fe 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy @@ -1820,32 +1820,6 @@ class SchemaGeneratorTest extends Specification { assert schema != null } - def "transformers get called once the schema is built"() { - def spec = """ - type Query { - hello: String - } - """ - - def types = new SchemaParser().parse(spec) - - def extraDirective = (GraphQLDirective.newDirective()).name("extra") - .argument(GraphQLArgument.newArgument().name("value").type(GraphQLString)).build() - def transformer = new SchemaGeneratorPostProcessing() { // Retained to show deprecated code is still run - @Override - GraphQLSchema process(GraphQLSchema originalSchema) { - originalSchema.transform({ builder -> builder.additionalDirective(extraDirective) }) - } - } - def wiring = RuntimeWiring.newRuntimeWiring() - .transformer(transformer) // Retained to show deprecated code is still run - .build() - GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(types, wiring) - expect: - assert schema != null - schema.getDirective("extra") != null - } - def "enum object default values are handled"() { def spec = ''' enum EnumValue { From 522a540dd4880c2f23dcc9c9a4277d038bcafc9b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 10 Mar 2024 19:23:53 +1100 Subject: [PATCH 309/393] Removed deprecated methods in ValidationError --- .../graphql/validation/ValidationError.java | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/main/java/graphql/validation/ValidationError.java b/src/main/java/graphql/validation/ValidationError.java index 7f456e0037..04c1f88936 100644 --- a/src/main/java/graphql/validation/ValidationError.java +++ b/src/main/java/graphql/validation/ValidationError.java @@ -23,46 +23,6 @@ public class ValidationError implements GraphQLError { private final List queryPath = new ArrayList<>(); private final ImmutableMap extensions; - @Deprecated(since = "2022-07-10", forRemoval = true) - public ValidationError(ValidationErrorClassification validationErrorType) { - this(newValidationError() - .validationErrorType(validationErrorType)); - } - - @Deprecated(since = "2022-07-10", forRemoval = true) - public ValidationError(ValidationErrorClassification validationErrorType, SourceLocation sourceLocation, String description) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocation(sourceLocation) - .description(description)); - } - - @Deprecated(since = "2022-07-10", forRemoval = true) - public ValidationError(ValidationErrorType validationErrorType, SourceLocation sourceLocation, String description, List queryPath) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocation(sourceLocation) - .description(description) - .queryPath(queryPath)); - } - - @Deprecated(since = "2022-07-10", forRemoval = true) - public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocations(sourceLocations) - .description(description)); - } - - @Deprecated(since = "2022-07-10", forRemoval = true) - public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description, List queryPath) { - this(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocations(sourceLocations) - .description(description) - .queryPath(queryPath)); - } - private ValidationError(Builder builder) { this.validationErrorType = builder.validationErrorType; this.description = builder.description; From e887ca117fcd1ae990ac5929367a584eee7216a0 Mon Sep 17 00:00:00 2001 From: Franklin Wang Date: Mon, 11 Mar 2024 14:43:31 +1100 Subject: [PATCH 310/393] Return empty singleton DirectivesHolder if directives are empty --- src/main/java/graphql/DirectivesUtil.java | 10 ++++++++-- src/main/java/graphql/schema/GraphQLArgument.java | 2 +- src/main/java/graphql/schema/GraphQLEnumType.java | 2 +- .../graphql/schema/GraphQLEnumValueDefinition.java | 2 +- .../java/graphql/schema/GraphQLFieldDefinition.java | 2 +- .../java/graphql/schema/GraphQLInputObjectField.java | 2 +- .../java/graphql/schema/GraphQLInputObjectType.java | 2 +- src/main/java/graphql/schema/GraphQLInterfaceType.java | 2 +- src/main/java/graphql/schema/GraphQLObjectType.java | 2 +- src/main/java/graphql/schema/GraphQLScalarType.java | 2 +- src/main/java/graphql/schema/GraphQLUnionType.java | 2 +- 11 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/DirectivesUtil.java b/src/main/java/graphql/DirectivesUtil.java index 0a560f7233..c539cfcd89 100644 --- a/src/main/java/graphql/DirectivesUtil.java +++ b/src/main/java/graphql/DirectivesUtil.java @@ -91,7 +91,6 @@ public static GraphQLDirective getFirstDirective(String name, Map toAppliedDirectives(GraphQLDirectiveContainer directiveContainer) { @@ -104,7 +103,6 @@ public static List toAppliedDirectives(GraphQLDirective * * @param appliedDirectives the applied directives to use * @param directives the legacy directives to use - * * @return a combined list unique by name */ public static List toAppliedDirectives(Collection appliedDirectives, Collection directives) { @@ -127,6 +125,7 @@ public static List toAppliedDirectives(Collection> allDirectivesByName; private final ImmutableMap nonRepeatableDirectivesByName; @@ -145,7 +144,14 @@ public DirectivesHolder(Collection allDirectives, Collection directives, List appliedDirectives) { + if (directives.isEmpty() && appliedDirectives.isEmpty()) { + return EMPTY_HOLDER; + } + return new DirectivesHolder(directives, appliedDirectives); } public ImmutableMap> getAllDirectivesByName() { diff --git a/src/main/java/graphql/schema/GraphQLArgument.java b/src/main/java/graphql/schema/GraphQLArgument.java index 99919b48b2..5f52f294cb 100644 --- a/src/main/java/graphql/schema/GraphQLArgument.java +++ b/src/main/java/graphql/schema/GraphQLArgument.java @@ -82,7 +82,7 @@ private GraphQLArgument(String name, this.value = value; this.definition = definition; this.deprecationReason = deprecationReason; - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); } diff --git a/src/main/java/graphql/schema/GraphQLEnumType.java b/src/main/java/graphql/schema/GraphQLEnumType.java index 00ced1b451..0e9d739500 100644 --- a/src/main/java/graphql/schema/GraphQLEnumType.java +++ b/src/main/java/graphql/schema/GraphQLEnumType.java @@ -66,7 +66,7 @@ private GraphQLEnumType(String name, this.description = description; this.definition = definition; this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions); - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.valueDefinitionMap = buildMap(values); } diff --git a/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java b/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java index 4d34417773..4aba114c7b 100644 --- a/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java +++ b/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java @@ -48,7 +48,7 @@ private GraphQLEnumValueDefinition(String name, this.description = description; this.value = value; this.deprecationReason = deprecationReason; - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.definition = definition; } diff --git a/src/main/java/graphql/schema/GraphQLFieldDefinition.java b/src/main/java/graphql/schema/GraphQLFieldDefinition.java index c717c929a2..8a06406439 100644 --- a/src/main/java/graphql/schema/GraphQLFieldDefinition.java +++ b/src/main/java/graphql/schema/GraphQLFieldDefinition.java @@ -66,7 +66,7 @@ private GraphQLFieldDefinition(String name, this.originalType = type; this.dataFetcherFactory = dataFetcherFactory; this.arguments = ImmutableList.copyOf(arguments); - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.deprecationReason = deprecationReason; this.definition = definition; } diff --git a/src/main/java/graphql/schema/GraphQLInputObjectField.java b/src/main/java/graphql/schema/GraphQLInputObjectField.java index 9a9aa0a85f..60e680fb30 100644 --- a/src/main/java/graphql/schema/GraphQLInputObjectField.java +++ b/src/main/java/graphql/schema/GraphQLInputObjectField.java @@ -62,7 +62,7 @@ private GraphQLInputObjectField( this.originalType = type; this.defaultValue = defaultValue; this.description = description; - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.definition = definition; this.deprecationReason = deprecationReason; } diff --git a/src/main/java/graphql/schema/GraphQLInputObjectType.java b/src/main/java/graphql/schema/GraphQLInputObjectType.java index 81b0d63160..9929f93493 100644 --- a/src/main/java/graphql/schema/GraphQLInputObjectType.java +++ b/src/main/java/graphql/schema/GraphQLInputObjectType.java @@ -61,7 +61,7 @@ private GraphQLInputObjectType(String name, this.description = description; this.definition = definition; this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions); - this.directives = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directives = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.fieldMap = buildDefinitionMap(fields); this.isOneOf = hasOneOf(directives, appliedDirectives); } diff --git a/src/main/java/graphql/schema/GraphQLInterfaceType.java b/src/main/java/graphql/schema/GraphQLInterfaceType.java index e0159a5ee0..765d4bfadc 100644 --- a/src/main/java/graphql/schema/GraphQLInterfaceType.java +++ b/src/main/java/graphql/schema/GraphQLInterfaceType.java @@ -77,7 +77,7 @@ private GraphQLInterfaceType(String name, this.interfaceComparator = interfaceComparator; this.originalInterfaces = ImmutableList.copyOf(sortTypes(interfaceComparator, interfaces)); this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions); - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.fieldDefinitionsByName = buildDefinitionMap(fieldDefinitions); } diff --git a/src/main/java/graphql/schema/GraphQLObjectType.java b/src/main/java/graphql/schema/GraphQLObjectType.java index b687215a68..4aa0a17003 100644 --- a/src/main/java/graphql/schema/GraphQLObjectType.java +++ b/src/main/java/graphql/schema/GraphQLObjectType.java @@ -74,7 +74,7 @@ private GraphQLObjectType(String name, this.originalInterfaces = ImmutableList.copyOf(sortTypes(interfaceComparator, interfaces)); this.definition = definition; this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions); - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.fieldDefinitionsByName = buildDefinitionMap(fieldDefinitions); } diff --git a/src/main/java/graphql/schema/GraphQLScalarType.java b/src/main/java/graphql/schema/GraphQLScalarType.java index 137a6047c0..fccfb0013b 100644 --- a/src/main/java/graphql/schema/GraphQLScalarType.java +++ b/src/main/java/graphql/schema/GraphQLScalarType.java @@ -64,7 +64,7 @@ private GraphQLScalarType(String name, this.description = description; this.coercing = coercing; this.definition = definition; - this.directivesHolder = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directivesHolder = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions); this.specifiedByUrl = specifiedByUrl; } diff --git a/src/main/java/graphql/schema/GraphQLUnionType.java b/src/main/java/graphql/schema/GraphQLUnionType.java index 6c5fffce39..9af647c6c4 100644 --- a/src/main/java/graphql/schema/GraphQLUnionType.java +++ b/src/main/java/graphql/schema/GraphQLUnionType.java @@ -69,7 +69,7 @@ private GraphQLUnionType(String name, this.typeResolver = typeResolver; this.definition = definition; this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions); - this.directives = new DirectivesUtil.DirectivesHolder(directives, appliedDirectives); + this.directives = DirectivesUtil.DirectivesHolder.create(directives, appliedDirectives); } void replaceTypes(List types) { From 9d41e986c7caf086c85d84d9cf35e5c293f9a53a Mon Sep 17 00:00:00 2001 From: Franklin Wang Date: Mon, 11 Mar 2024 15:52:47 +1100 Subject: [PATCH 311/393] Fix build --- src/main/java/graphql/DirectivesUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/DirectivesUtil.java b/src/main/java/graphql/DirectivesUtil.java index c539cfcd89..e4a3d45d90 100644 --- a/src/main/java/graphql/DirectivesUtil.java +++ b/src/main/java/graphql/DirectivesUtil.java @@ -9,6 +9,7 @@ import graphql.util.FpKit; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -125,7 +126,7 @@ public static List toAppliedDirectives(Collection> allDirectivesByName; private final ImmutableMap nonRepeatableDirectivesByName; From 8000acce51a2cfa1cc6f974b853fb001e98e2502 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:19:59 +0000 Subject: [PATCH 312/393] Bump com.fasterxml.jackson.core:jackson-databind from 2.16.1 to 2.16.2 Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.16.1 to 2.16.2. - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a46ff7c393..08662b8725 100644 --- a/build.gradle +++ b/build.gradle @@ -111,7 +111,7 @@ dependencies { testImplementation 'org.codehaus.groovy:groovy-json:3.0.21' testImplementation 'com.google.code.gson:gson:2.10.1' testImplementation 'org.eclipse.jetty:jetty-server:11.0.20' - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.2' testImplementation 'org.awaitility:awaitility-groovy:4.2.0' testImplementation 'com.github.javafaker:javafaker:1.0.2' From f4744d628f4663369dc94c48ae44bdee45f51ba8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:20:07 +0000 Subject: [PATCH 313/393] Bump org.assertj:assertj-core from 3.25.1 to 3.25.3 Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.25.1 to 3.25.3. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.25.1...assertj-build-3.25.3) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- agent-test/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent-test/build.gradle b/agent-test/build.gradle index f49f603f71..b383c5eeab 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -9,7 +9,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testImplementation("org.assertj:assertj-core:3.25.1") + testImplementation("org.assertj:assertj-core:3.25.3") } From eb218925e7acd7dd368b0dfc01ad041fe633ed33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:20:15 +0000 Subject: [PATCH 314/393] Bump org.junit.jupiter:junit-jupiter from 5.7.1 to 5.10.2 Bumps [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit5) from 5.7.1 to 5.10.2. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.1...r5.10.2) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- agent-test/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent-test/build.gradle b/agent-test/build.gradle index f49f603f71..c0ff4f5f1a 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -6,7 +6,7 @@ dependencies { implementation(rootProject) implementation("net.bytebuddy:byte-buddy-agent:1.14.11") - testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation("org.assertj:assertj-core:3.25.1") From a49166e2ea6dc8265b2f7a9e18e85eb791d790b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 02:52:28 +0000 Subject: [PATCH 315/393] Bump net.bytebuddy:byte-buddy from 1.14.11 to 1.14.12 Bumps [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) from 1.14.11 to 1.14.12. - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.11...byte-buddy-1.14.12) --- updated-dependencies: - dependency-name: net.bytebuddy:byte-buddy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- agent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/build.gradle b/agent/build.gradle index af23b0c5c6..754e8af406 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -6,7 +6,7 @@ plugins { } dependencies { - implementation("net.bytebuddy:byte-buddy:1.14.11") + implementation("net.bytebuddy:byte-buddy:1.14.12") // graphql-java itself implementation(rootProject) } From b33401ff5c001dc362ae4be4bf56111e51094ad6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 02:52:33 +0000 Subject: [PATCH 316/393] Bump net.bytebuddy:byte-buddy-agent from 1.14.11 to 1.14.12 Bumps [net.bytebuddy:byte-buddy-agent](https://github.com/raphw/byte-buddy) from 1.14.11 to 1.14.12. - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.11...byte-buddy-1.14.12) --- updated-dependencies: - dependency-name: net.bytebuddy:byte-buddy-agent dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- agent-test/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent-test/build.gradle b/agent-test/build.gradle index cce8a4cd58..288fd50045 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -4,7 +4,7 @@ plugins { dependencies { implementation(rootProject) - implementation("net.bytebuddy:byte-buddy-agent:1.14.11") + implementation("net.bytebuddy:byte-buddy-agent:1.14.12") testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' From ccaf737f50e5a5fe336c00ada7b2156de1f24f67 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 15 Mar 2024 09:46:02 +1000 Subject: [PATCH 317/393] allow for a max result nodes limit for execution --- .../graphql/execution/ExecutionContext.java | 6 + .../graphql/execution/ExecutionStrategy.java | 23 ++++ src/test/groovy/graphql/GraphQLTest.groovy | 130 ++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 71f6c1827b..0943084bfc 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -27,6 +27,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; @@ -59,6 +60,7 @@ public class ExecutionContext { private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; + private final AtomicLong resultNodesCount = new AtomicLong(); // this is modified after creation so it needs to be volatile to ensure visibility across Threads private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; @@ -304,4 +306,8 @@ public ExecutionContext transform(Consumer builderConsu builderConsumer.accept(builder); return builder.build(); } + + public AtomicLong getResultNodesCount() { + return resultNodesCount; + } } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 8a58d1d9b3..e4ca2e98df 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -131,6 +131,10 @@ @SuppressWarnings("FutureReturnValueIgnored") public abstract class ExecutionStrategy { + @Internal + public static final String MAX_RESULT_NODES = "__MAX_RESULT_NODES"; + @Internal + public static final String MAX_RESULT_NODES_BREACHED = "__MAX_RESULT_NODES_BREACHED"; protected final FieldCollector fieldCollector = new FieldCollector(); protected final ExecutionStepInfoFactory executionStepInfoFactory = new ExecutionStepInfoFactory(); protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; @@ -381,6 +385,16 @@ protected CompletableFuture fetchField(ExecutionContext executionC } private CompletableFuture fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + + long resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); + Long maxNodes = null; + if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { + if (resultNodesCount > maxNodes) { + executionContext.getGraphQLContext().put(MAX_RESULT_NODES_BREACHED, true); + return CompletableFuture.completedFuture(new FetchedValue(null, Collections.emptyList(), null)); + } + } + MergedField field = parameters.getField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); @@ -712,6 +726,15 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, List fieldValueInfos = new ArrayList<>(size.orElse(1)); int index = 0; for (Object item : iterableValues) { + long resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); + Long maxNodes; + if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { + if (resultNodesCount > maxNodes) { + executionContext.getGraphQLContext().put(MAX_RESULT_NODES_BREACHED, true); + return new FieldValueInfo(NULL, completedFuture(null), fieldValueInfos); + } + } + ResultPath indexedPath = parameters.getPath().segment(index); ExecutionStepInfo stepInfoForListElement = executionStepInfoFactory.newExecutionStepInfoForListElement(executionStepInfo, indexedPath); diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index 999453bd5a..af1e3826e9 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -11,6 +11,7 @@ import graphql.execution.DataFetcherResult import graphql.execution.ExecutionContext import graphql.execution.ExecutionId import graphql.execution.ExecutionIdProvider +import graphql.execution.ExecutionStrategy import graphql.execution.ExecutionStrategyParameters import graphql.execution.MissingRootTypeException import graphql.execution.SubscriptionExecutionStrategy @@ -1427,4 +1428,133 @@ many lines'''] then: !er.errors.isEmpty() } + + def "max result nodes not breached"() { + given: + def sdl = ''' + + type Query { + hello: String + } + ''' + def df = { env -> "world" } as DataFetcher + def fetchers = ["Query": ["hello": df]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ hello h1: hello h2: hello h3: hello } " + def ei = newExecutionInput(query).build() + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 4L); + + when: + def er = graphQL.execute(ei) + then: + ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == null + er.data == [hello: "world", h1: "world", h2: "world", h3: "world"] + } + + def "max result nodes breached"() { + given: + def sdl = ''' + + type Query { + hello: String + } + ''' + def df = { env -> "world" } as DataFetcher + def fetchers = ["Query": ["hello": df]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ hello h1: hello h2: hello h3: hello } " + def ei = newExecutionInput(query).build() + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3L); + + when: + def er = graphQL.execute(ei) + then: + ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == true + er.data == [hello: "world", h1: "world", h2: "world", h3: null] + } + + def "max result nodes breached with list"() { + given: + def sdl = ''' + + type Query { + hello: [String] + } + ''' + def df = { env -> ["w1", "w2", "w3"] } as DataFetcher + def fetchers = ["Query": ["hello": df]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ hello}" + def ei = newExecutionInput(query).build() + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3L); + + when: + def er = graphQL.execute(ei) + then: + ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == true + er.data == [hello: null] + } + + def "max result nodes breached with list 2"() { + given: + def sdl = ''' + + type Query { + hello: [Foo] + } + type Foo { + name: String + } + ''' + def df = { env -> [[name: "w1"], [name: "w2"], [name: "w3"]] } as DataFetcher + def fetchers = ["Query": ["hello": df]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ hello {name}}" + def ei = newExecutionInput(query).build() + // we have 7 result nodes overall + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 6L); + + when: + def er = graphQL.execute(ei) + then: + ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == true + er.data == [hello: [[name: "w1"], [name: "w2"], [name: null]]] + } + + def "max result nodes not breached with list"() { + given: + def sdl = ''' + + type Query { + hello: [Foo] + } + type Foo { + name: String + } + ''' + def df = { env -> [[name: "w1"], [name: "w2"], [name: "w3"]] } as DataFetcher + def fetchers = ["Query": ["hello": df]] + def schema = TestUtil.schema(sdl, fetchers) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = "{ hello {name}}" + def ei = newExecutionInput(query).build() + // we have 7 result nodes overall + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 7L); + + when: + def er = graphQL.execute(ei) + then: + ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == null + er.data == [hello: [[name: "w1"], [name: "w2"], [name: "w3"]]] + } + } From 3570606290b0fdc9cc9a9d19b97d469ed3494ed0 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 15 Mar 2024 09:56:58 +1000 Subject: [PATCH 318/393] int instead of long --- src/main/java/graphql/execution/ExecutionContext.java | 6 +++--- src/main/java/graphql/execution/ExecutionStrategy.java | 8 ++++---- src/test/groovy/graphql/GraphQLTest.groovy | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 0943084bfc..935965f915 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -27,7 +27,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; @@ -60,7 +60,7 @@ public class ExecutionContext { private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; - private final AtomicLong resultNodesCount = new AtomicLong(); + private final AtomicInteger resultNodesCount = new AtomicInteger(); // this is modified after creation so it needs to be volatile to ensure visibility across Threads private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; @@ -307,7 +307,7 @@ public ExecutionContext transform(Consumer builderConsu return builder.build(); } - public AtomicLong getResultNodesCount() { + public AtomicInteger getResultNodesCount() { return resultNodesCount; } } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index e4ca2e98df..b24c4cfe35 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -386,8 +386,8 @@ protected CompletableFuture fetchField(ExecutionContext executionC private CompletableFuture fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - long resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); - Long maxNodes = null; + int resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); + Integer maxNodes; if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { if (resultNodesCount > maxNodes) { executionContext.getGraphQLContext().put(MAX_RESULT_NODES_BREACHED, true); @@ -726,8 +726,8 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, List fieldValueInfos = new ArrayList<>(size.orElse(1)); int index = 0; for (Object item : iterableValues) { - long resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); - Long maxNodes; + int resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); + Integer maxNodes; if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { if (resultNodesCount > maxNodes) { executionContext.getGraphQLContext().put(MAX_RESULT_NODES_BREACHED, true); diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index af1e3826e9..4bb75d0b76 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -1444,7 +1444,7 @@ many lines'''] def query = "{ hello h1: hello h2: hello h3: hello } " def ei = newExecutionInput(query).build() - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 4L); + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 4); when: def er = graphQL.execute(ei) @@ -1468,7 +1468,7 @@ many lines'''] def query = "{ hello h1: hello h2: hello h3: hello } " def ei = newExecutionInput(query).build() - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3L); + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3); when: def er = graphQL.execute(ei) @@ -1492,7 +1492,7 @@ many lines'''] def query = "{ hello}" def ei = newExecutionInput(query).build() - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3L); + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3); when: def er = graphQL.execute(ei) @@ -1520,7 +1520,7 @@ many lines'''] def query = "{ hello {name}}" def ei = newExecutionInput(query).build() // we have 7 result nodes overall - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 6L); + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 6); when: def er = graphQL.execute(ei) @@ -1548,7 +1548,7 @@ many lines'''] def query = "{ hello {name}}" def ei = newExecutionInput(query).build() // we have 7 result nodes overall - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 7L); + ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 7); when: def er = graphQL.execute(ei) From 7ab747ee18ce2cc17f4f65ac5e7720f36d4bf1a9 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 15 Mar 2024 21:58:10 +1100 Subject: [PATCH 319/393] This provides and ability to disable Introspection --- src/main/java/graphql/ErrorType.java | 1 + .../graphql/introspection/Introspection.java | 68 ++++++++++++++++++- .../IntrospectionDisabledError.java | 35 ++++++++++ .../introspection/IntrospectionTest.groovy | 57 +++++++++++++++- 4 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 src/main/java/graphql/introspection/IntrospectionDisabledError.java diff --git a/src/main/java/graphql/ErrorType.java b/src/main/java/graphql/ErrorType.java index 9adee6d461..f9f01582dc 100644 --- a/src/main/java/graphql/ErrorType.java +++ b/src/main/java/graphql/ErrorType.java @@ -11,5 +11,6 @@ public enum ErrorType implements ErrorClassification { DataFetchingException, NullValueInNonNullableField, OperationNotSupported, + IntrospectionDisabled, ExecutionAborted } diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index d496e03702..05f0ba095d 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -6,8 +6,10 @@ import graphql.GraphQLContext; import graphql.Internal; import graphql.PublicApi; +import graphql.execution.DataFetcherResult; import graphql.execution.ValuesResolver; import graphql.language.AstPrinter; +import graphql.language.Field; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLCodeRegistry; @@ -40,6 +42,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; @@ -58,8 +61,55 @@ import static graphql.schema.GraphQLTypeUtil.unwrapAllAs; import static graphql.schema.GraphQLTypeUtil.unwrapOne; +/** + * GraphQl has a unique capability called Introspection that allow + * consumers to inspect the system and discover the fields and types available and makes the system self documented. + *

+ * Some security recommendations such as OWASP + * recommend that introspection be disabled in production. The {@link Introspection#enabledJvmWide(boolean)} method can be used to disable + * introspection for the whole JVM or you can place {@link Introspection#INTROSPECTION_DISABLED} into the {@link GraphQLContext} of a request + * to disable introspection for that request. + */ @PublicApi public class Introspection { + + + /** + * Placing a boolean value under this key in the per request {@link GraphQLContext} will enable + * or disable Introspection on that request. + */ + public static final String INTROSPECTION_DISABLED = "INTROSPECTION_DISABLED"; + private static final AtomicBoolean INTROSPECTION_ENABLED_STATE = new AtomicBoolean(true); + + /** + * This static method will enable / disable Introspection at a JVM wide level. + * + * @param enabled the flag indicating the desired enabled state + * + * @return the previous state of enablement + */ + public static boolean enabledJvmWide(boolean enabled) { + return INTROSPECTION_ENABLED_STATE.getAndSet(enabled); + } + + /** + * @return true if Introspection is enabled at a JVM wide level or false otherwise + */ + public static boolean isEnabledJvmWide() { + return INTROSPECTION_ENABLED_STATE.get(); + } + + private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { + if (!isEnabledJvmWide()) { + return false; + } + return ! graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false); + } + + private static Object introspectionDisabledError(Field field) { + return DataFetcherResult.newResult().data(null).error(new IntrospectionDisabledError(field.getSourceLocation())).build(); + } + private static final Map> introspectionDataFetchers = new LinkedHashMap<>(); private static void register(GraphQLFieldsContainer parentType, String fieldName, IntrospectionDataFetcher introspectionDataFetcher) { @@ -629,13 +679,25 @@ public enum DirectiveLocation { Introspection.TypeNameMetaFieldDef.getName() ); - public static final IntrospectionDataFetcher SchemaMetaFieldDefDataFetcher = IntrospectionDataFetchingEnvironment::getGraphQLSchema; + public static final IntrospectionDataFetcher SchemaMetaFieldDefDataFetcher = environment -> { + if (isIntrospectionEnabled(environment.getGraphQlContext())) { + return environment.getGraphQLSchema(); + } else { + return introspectionDisabledError(environment.getField()); + } + }; + public static final IntrospectionDataFetcher TypeMetaFieldDefDataFetcher = environment -> { - String name = environment.getArgument("name"); - return environment.getGraphQLSchema().getType(name); + if (isIntrospectionEnabled(environment.getGraphQlContext())) { + String name = environment.getArgument("name"); + return environment.getGraphQLSchema().getType(name); + } else { + return introspectionDisabledError(environment.getField()); + } }; + // __typename is always available public static final IntrospectionDataFetcher TypeNameMetaFieldDefDataFetcher = environment -> simplePrint(environment.getParentType()); @Internal diff --git a/src/main/java/graphql/introspection/IntrospectionDisabledError.java b/src/main/java/graphql/introspection/IntrospectionDisabledError.java new file mode 100644 index 0000000000..d48a8cca05 --- /dev/null +++ b/src/main/java/graphql/introspection/IntrospectionDisabledError.java @@ -0,0 +1,35 @@ +package graphql.introspection; + +import graphql.ErrorClassification; +import graphql.ErrorType; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.language.SourceLocation; + +import java.util.Collections; +import java.util.List; + +@Internal +public class IntrospectionDisabledError implements GraphQLError { + + private final List locations; + + public IntrospectionDisabledError(SourceLocation sourceLocation) { + locations = sourceLocation == null ? Collections.emptyList() : Collections.singletonList(sourceLocation); + } + + @Override + public String getMessage() { + return "Introspection has been disabled for this request"; + } + + @Override + public List getLocations() { + return locations; + } + + @Override + public ErrorClassification getErrorType() { + return ErrorType.IntrospectionDisabled; + } +} diff --git a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy index 62e27138d1..9c6abe3c91 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy @@ -1,6 +1,7 @@ package graphql.introspection - +import graphql.ErrorType +import graphql.ExecutionInput import graphql.TestUtil import graphql.schema.DataFetcher import graphql.schema.FieldCoordinates @@ -688,4 +689,58 @@ class IntrospectionTest extends Specification { queryType["isOneOf"] == null } + def "jvm wide enablement"() { + def graphQL = TestUtil.graphQL("type Query { f : String } ").build() + + when: + def er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + + when: + Introspection.enabledJvmWide(false) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + + when: + Introspection.enabledJvmWide(true) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + } + + def "per request enablement"() { + def graphQL = TestUtil.graphQL("type Query { f : String } ").build() + + when: + // null context + def ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY) + .build() + def er = graphQL.execute(ei) + + then: + er.errors.isEmpty() + + when: + ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY) + .graphQLContext(Map.of(Introspection.INTROSPECTION_DISABLED, false)).build() + er = graphQL.execute(ei) + + then: + er.errors.isEmpty() + + when: + ei = ExecutionInput.newExecutionInput(IntrospectionQuery.INTROSPECTION_QUERY) + .graphQLContext(Map.of(Introspection.INTROSPECTION_DISABLED, true)).build() + er = graphQL.execute(ei) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + } } From 40eae76e710fe123efdfa3c768a39a5f0ff16454 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 16 Mar 2024 08:24:20 +1100 Subject: [PATCH 320/393] This provides and ability to disable Introspection - deprecate NoIntrospectionGraphqlFieldVisibility --- .../visibility/NoIntrospectionGraphqlFieldVisibility.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java b/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java index 604e794114..62aa16bbe0 100644 --- a/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java +++ b/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java @@ -12,10 +12,15 @@ * This field visibility will prevent Introspection queries from being performed. Technically this puts your * system in contravention of the specification * but some production systems want this lock down in place. + * + * @deprecated This is no longer the best way to prevent Introspection - {@link graphql.introspection.Introspection#enabledJvmWide(boolean)} + * can be used instead */ @PublicApi +@Deprecated(since = "2024-03-16") public class NoIntrospectionGraphqlFieldVisibility implements GraphqlFieldVisibility { + @Deprecated(since = "2024-03-16") public static NoIntrospectionGraphqlFieldVisibility NO_INTROSPECTION_FIELD_VISIBILITY = new NoIntrospectionGraphqlFieldVisibility(); From b361146acf3d11996fc3905d2d66199faf3700fd Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 16 Mar 2024 12:26:23 +1100 Subject: [PATCH 321/393] This provides a GoodFaithIntrospectionInstrumentation option --- ...GoodFaithIntrospectionInstrumentation.java | 87 +++++++++++++++++++ ...ithIntrospectionInstrumentationTest.groovy | 67 ++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java create mode 100644 src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java b/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java new file mode 100644 index 0000000000..39841f2159 --- /dev/null +++ b/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java @@ -0,0 +1,87 @@ +package graphql.introspection; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import graphql.PublicApi; +import graphql.execution.AbortExecutionException; +import graphql.execution.ExecutionContext; +import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.normalized.ExecutableNormalizedField; +import graphql.normalized.ExecutableNormalizedOperation; +import graphql.schema.FieldCoordinates; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +import static graphql.schema.FieldCoordinates.coordinates; + +/** + * This {@link graphql.execution.instrumentation.Instrumentation} ensure that a submitted introspection query is done in + * good faith. + *

+ * There are attack vectors where a crafted introspection query can cause the engine to spend too much time + * producing introspection data. This is especially true on large schemas with lots of types and fields. + *

+ * Schemas form a cyclic graph and hence it's possible to send in introspection queries that can reference those cycles + * and in large schemas this can be expensive and perhaps a "denial of service". + *

+ * This instrumentation only allows one __schema field or one __type field to be present, and it does not allow the `__Type` fields + * to form a cycle, i.e., that can only be present once. This allows the standard and common introspection queries to work + * so tooling such as graphiql can work. + */ +@PublicApi +public class GoodFaithIntrospectionInstrumentation extends SimplePerformantInstrumentation { + + private static final Map ALLOWED_FIELD_INSTANCES = Map.of( + coordinates("Query", "__schema"), 1 + , coordinates("Query", "__type"), 1 + + , coordinates("__Type", "fields"), 1 + , coordinates("__Type", "inputFields"), 1 + , coordinates("__Type", "interfaces"), 1 + , coordinates("__Type", "possibleTypes"), 1 + ); + + @Override + public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + + // We check first to see if there are any introspection fields present on the top level field set + // if there isn't, then there is nothing to check. This ensures we are performant. + List topLevelFieldName = parameters.getExecutionStrategyParameters().getFields().getKeys(); + boolean weHaveIntrospectionFields = false; + for (String fieldName : topLevelFieldName) { + if (Introspection.SchemaMetaFieldDef.getName().equals(fieldName) || Introspection.TypeMetaFieldDef.getName().equals(fieldName)) { + weHaveIntrospectionFields = true; + break; + } + } + if (weHaveIntrospectionFields) { + ensureTheyAreInGoodFaith(parameters.getExecutionContext()); + } + return ExecutionStrategyInstrumentationContext.NOOP; + } + + private void ensureTheyAreInGoodFaith(ExecutionContext executionContext) { + ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get(); + ImmutableListMultimap coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); + for (Map.Entry entry : ALLOWED_FIELD_INSTANCES.entrySet()) { + FieldCoordinates coordinates = entry.getKey(); + Integer allowSize = entry.getValue(); + ImmutableList normalizedFields = coordinatesToENFs.get(coordinates); + if (normalizedFields.size() > allowSize) { + throw new BadFaithIntrospectionAbortExecutionException(coordinates.toString()); + } + } + } + + @PublicApi + public static class BadFaithIntrospectionAbortExecutionException extends AbortExecutionException { + public BadFaithIntrospectionAbortExecutionException(String qualifiedField) { + super(String.format("This request is not asking for introspection in good faith - %s is present too often!", qualifiedField)); + } + } +} diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy new file mode 100644 index 0000000000..ad0d057a5f --- /dev/null +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -0,0 +1,67 @@ +package graphql.introspection + +import graphql.ExecutionResult +import graphql.TestUtil +import spock.lang.Specification + +class GoodFaithIntrospectionInstrumentationTest extends Specification { + + def graphql = TestUtil.graphQL("type Query { f : String }").instrumentation(new GoodFaithIntrospectionInstrumentation()).build() + + def "test asking for introspection in good faith"() { + + when: + ExecutionResult er = graphql.execute(IntrospectionQuery.INTROSPECTION_QUERY) + then: + er.errors.isEmpty() + } + + def "test asking for introspection in bad faith"() { + + when: + ExecutionResult er = graphql.execute(query) + then: + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + + where: + query | _ + // a case for __Type interfaces + """ query badActor { + __schema { types { interfaces { fields { type { interfaces { name } } } } } } + } + """ | _ + // a case for __Type inputFields + """ query badActor { + __schema { types { inputFields { type { inputFields { name }}}}} + } + """ | _ + // a case for __Type possibleTypes + """ query badActor { + __schema { types { inputFields { type { inputFields { name }}}}} + } + """ | _ + // a case leading from __InputValue + """ query badActor { + __schema { types { fields { args { type { name fields { name }}}}}} + } + """ | _ + // a case leading from __Field + """ query badActor { + __schema { types { fields { type { name fields { name }}}}} + } + """ | _ + // a case for __type + """ query badActor { + __type(name : "t") { name } + alias1 : __type(name : "t1") { name } + } + """ | _ + // a case for schema repeated - dont ask twice + """ query badActor { + __schema { types { name} } + alias1 : __schema { types { name} } + } + """ | _ + } +} From d719d4fd5786adec3e4f07313d2bdcf5e853110d Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 16 Mar 2024 13:12:41 +1100 Subject: [PATCH 322/393] This provides a GoodFaithIntrospectionInstrumentation option - longer attack test --- ...ithIntrospectionInstrumentationTest.groovy | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy index ad0d057a5f..318af0649c 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -25,43 +25,47 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException where: - query | _ + query | _ + // long attack + """ + query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}} + """ | _ // a case for __Type interfaces """ query badActor { __schema { types { interfaces { fields { type { interfaces { name } } } } } } } - """ | _ + """ | _ // a case for __Type inputFields """ query badActor { __schema { types { inputFields { type { inputFields { name }}}}} } - """ | _ + """ | _ // a case for __Type possibleTypes """ query badActor { __schema { types { inputFields { type { inputFields { name }}}}} } - """ | _ + """ | _ // a case leading from __InputValue """ query badActor { __schema { types { fields { args { type { name fields { name }}}}}} } - """ | _ + """ | _ // a case leading from __Field """ query badActor { __schema { types { fields { type { name fields { name }}}}} } - """ | _ + """ | _ // a case for __type """ query badActor { __type(name : "t") { name } alias1 : __type(name : "t1") { name } } - """ | _ + """ | _ // a case for schema repeated - dont ask twice """ query badActor { __schema { types { name} } alias1 : __schema { types { name} } } - """ | _ + """ | _ } } From d304767e32d234b37d2958098c12c94536ad723e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 16 Mar 2024 15:17:33 +1100 Subject: [PATCH 323/393] This provides a GoodFaithIntrospectionInstrumentation option - now with builder to allow you to specify extras if you want --- ...GoodFaithIntrospectionInstrumentation.java | 63 ++++++++++++++- ...ithIntrospectionInstrumentationTest.groovy | 81 +++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java b/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java index 39841f2159..d95567cd9c 100644 --- a/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java +++ b/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java @@ -14,6 +14,7 @@ import graphql.schema.FieldCoordinates; import org.jetbrains.annotations.Nullable; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -45,6 +46,15 @@ public class GoodFaithIntrospectionInstrumentation extends SimplePerformantInstr , coordinates("__Type", "interfaces"), 1 , coordinates("__Type", "possibleTypes"), 1 ); + private final Map allowFieldInstances; + + public GoodFaithIntrospectionInstrumentation() { + this(ALLOWED_FIELD_INSTANCES); + } + + private GoodFaithIntrospectionInstrumentation(Map allowFieldInstances) { + this.allowFieldInstances = allowFieldInstances; + } @Override public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { @@ -68,7 +78,7 @@ public class GoodFaithIntrospectionInstrumentation extends SimplePerformantInstr private void ensureTheyAreInGoodFaith(ExecutionContext executionContext) { ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get(); ImmutableListMultimap coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); - for (Map.Entry entry : ALLOWED_FIELD_INSTANCES.entrySet()) { + for (Map.Entry entry : allowFieldInstances.entrySet()) { FieldCoordinates coordinates = entry.getKey(); Integer allowSize = entry.getValue(); ImmutableList normalizedFields = coordinatesToENFs.get(coordinates); @@ -84,4 +94,55 @@ public BadFaithIntrospectionAbortExecutionException(String qualifiedField) { super(String.format("This request is not asking for introspection in good faith - %s is present too often!", qualifiedField)); } } + + public static Builder newGoodFaithIntrospection() { + return new Builder(); + } + + public static class Builder { + + + private final Map allowFieldInstances = new LinkedHashMap<>(ALLOWED_FIELD_INSTANCES); + + /** + * This allows you to set how many __type(name : "x) field instances are allowed in an introspection query + * + * @param maxInstances the number allowed + * + * @return this builder + */ + public Builder maximumUnderscoreTypeInstances(int maxInstances) { + allowFieldInstances.put(coordinates("Query", "__type"), maxInstances); + return this; + } + + /** + * This allows you to set how many __schema field instances are allowed in an introspection query + * + * @param maxInstances the number allowed + * + * @return this builder + */ + public Builder maximumUnderscoreSchemaInstances(int maxInstances) { + allowFieldInstances.put(coordinates("Query", "__schema"), maxInstances); + return this; + } + + /** + * This allows you to set how many qualified field instances are allowed in an introspection query + * + * @param coordinates - the qualified field name + * @param maxInstances the number allowed + * + * @return this builder + */ + public Builder maximumFieldInstances(FieldCoordinates coordinates, int maxInstances) { + allowFieldInstances.put(coordinates, maxInstances); + return this; + } + + public GoodFaithIntrospectionInstrumentation build() { + return new GoodFaithIntrospectionInstrumentation(allowFieldInstances); + } + } } diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy index 318af0649c..1450aa456c 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -2,6 +2,7 @@ package graphql.introspection import graphql.ExecutionResult import graphql.TestUtil +import graphql.schema.FieldCoordinates import spock.lang.Specification class GoodFaithIntrospectionInstrumentationTest extends Specification { @@ -68,4 +69,84 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { } """ | _ } + + def "test builder"() { + GoodFaithIntrospectionInstrumentation goodFaithIntrospectionInstrumentation = GoodFaithIntrospectionInstrumentation.newGoodFaithIntrospection() + .maximumUnderscoreSchemaInstances(2) + .maximumUnderscoreTypeInstances(3) + .maximumFieldInstances(FieldCoordinates.coordinates("__Type", "fields"), 2) + .build() + + def graphql = TestUtil.graphQL("type Query { f : String }").instrumentation(goodFaithIntrospectionInstrumentation).build() + + when: + def er = graphql.execute(IntrospectionQuery.INTROSPECTION_QUERY) + then: + er.errors.isEmpty() + + when: + er = graphql.execute(""" + query ok { + __schema { types { name } } + alias1: __schema { types { name } } + } + """) + then: + er.errors.isEmpty() + + when: + er = graphql.execute(""" + query ok { + __schema { types { name } } + alias1: __schema { types { name } } + alias2: __schema { types { name } } + } + """) + then: + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + + when: + er = graphql.execute(""" + query ok { + __type(name : "Query") { name } + alias1 : __type(name : "Query") { name } + alias2 : __type(name : "Query") { name } + } + """) + then: + er.errors.isEmpty() + + when: + er = graphql.execute(""" + query ok { + __type(name : "Query") { name } + alias1 : __type(name : "Query") { name } + alias2 : __type(name : "Query") { name } + alias3 : __type(name : "Query") { name } + } + """) + then: + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + + when: + er = graphql.execute(""" + query ok { + __schema { types { fields { type { fields { name }}}}} + } + """) + then: + er.errors.isEmpty() + + when: + er = graphql.execute(""" + query ok { + __schema { types { fields { type { fields { type { fields { name }}}}}}} + } + """) + then: + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + } } From b1705b0b0702d680ecb4a59e8d4699ad38b4a0cd Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 17 Mar 2024 17:03:39 +1100 Subject: [PATCH 324/393] This moves the execution stopping into the ES --- .../execution/AsyncExecutionStrategy.java | 7 +++ .../AsyncSerialExecutionStrategy.java | 9 +++ .../graphql/introspection/Introspection.java | 62 ++++++++++++------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 3e640c9ace..6045bdf023 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -6,8 +6,10 @@ import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.introspection.Introspection; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; @@ -46,6 +48,11 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); + Optional isDisabled = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields); + if (isDisabled.isPresent()) { + return CompletableFuture.completedFuture(isDisabled.get()); + } + DeferredExecutionSupport deferredExecutionSupport = createDeferredExecutionSupport(executionContext, parameters); Async.CombinedBuilder futures = getAsyncFieldValueInfo(executionContext, parameters, deferredExecutionSupport); diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index e5772f052d..6dbff81130 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -6,8 +6,10 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.introspection.Introspection; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; @@ -40,6 +42,13 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); ImmutableList fieldNames = ImmutableList.copyOf(fields.keySet()); + // this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code + // so belts and braces + Optional isDisabled = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields); + if (isDisabled.isPresent()) { + return CompletableFuture.completedFuture(isDisabled.get()); + } + CompletableFuture> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, prevResults) -> { MergedField currentField = fields.getSubField(fieldName); ResultPath fieldPath = parameters.getPath().segment(mkNameForPath(currentField)); diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 05f0ba095d..4015ff393b 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -3,13 +3,14 @@ import com.google.common.collect.ImmutableSet; import graphql.Assert; +import graphql.ExecutionResult; import graphql.GraphQLContext; import graphql.Internal; import graphql.PublicApi; -import graphql.execution.DataFetcherResult; +import graphql.execution.MergedField; +import graphql.execution.MergedSelectionSet; import graphql.execution.ValuesResolver; import graphql.language.AstPrinter; -import graphql.language.Field; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLCodeRegistry; @@ -34,6 +35,7 @@ import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLUnionType; import graphql.schema.InputValueWithState; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashSet; @@ -41,6 +43,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -99,15 +102,43 @@ public static boolean isEnabledJvmWide() { return INTROSPECTION_ENABLED_STATE.get(); } + /** + * This will look in to the field selection set and see if there are introspection fields, + * and if there is,it checks if introspection should run, and if not it will return an errored {@link ExecutionResult} + * that can be returned to the user. + * + * @param mergedSelectionSet the fields to be executed + * + * @return an optional error result + */ + public static Optional isIntrospectionSensible(GraphQLContext graphQLContext, MergedSelectionSet mergedSelectionSet) { + MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName()); + if (schemaField != null) { + if (!isIntrospectionEnabled(graphQLContext)) { + return mkDisabledError(schemaField); + } + } + MergedField typeField = mergedSelectionSet.getSubField(TypeMetaFieldDef.getName()); + if (typeField != null) { + if (!isIntrospectionEnabled(graphQLContext)) { + return mkDisabledError(typeField); + } + } + // later we can put a good faith check code here to check the fields make sense + return Optional.empty(); + } + + @NotNull + private static Optional mkDisabledError(MergedField schemaField) { + IntrospectionDisabledError error = new IntrospectionDisabledError(schemaField.getSingleField().getSourceLocation()); + return Optional.of(ExecutionResult.newExecutionResult().addError(error).build()); + } + private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { if (!isEnabledJvmWide()) { return false; } - return ! graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false); - } - - private static Object introspectionDisabledError(Field field) { - return DataFetcherResult.newResult().data(null).error(new IntrospectionDisabledError(field.getSourceLocation())).build(); + return !graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false); } private static final Map> introspectionDataFetchers = new LinkedHashMap<>(); @@ -679,22 +710,11 @@ public enum DirectiveLocation { Introspection.TypeNameMetaFieldDef.getName() ); - public static final IntrospectionDataFetcher SchemaMetaFieldDefDataFetcher = environment -> { - if (isIntrospectionEnabled(environment.getGraphQlContext())) { - return environment.getGraphQLSchema(); - } else { - return introspectionDisabledError(environment.getField()); - } - }; - + public static final IntrospectionDataFetcher SchemaMetaFieldDefDataFetcher = IntrospectionDataFetchingEnvironment::getGraphQLSchema; public static final IntrospectionDataFetcher TypeMetaFieldDefDataFetcher = environment -> { - if (isIntrospectionEnabled(environment.getGraphQlContext())) { - String name = environment.getArgument("name"); - return environment.getGraphQLSchema().getType(name); - } else { - return introspectionDisabledError(environment.getField()); - } + String name = environment.getArgument("name"); + return environment.getGraphQLSchema().getType(name); }; // __typename is always available From 96053ef3f939e3e54c230e4ef6343a9dc4b04345 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 17 Mar 2024 17:09:59 +1100 Subject: [PATCH 325/393] This moves the execution stopping into the ES - better naming --- src/main/java/graphql/execution/AsyncExecutionStrategy.java | 6 +++--- .../graphql/execution/AsyncSerialExecutionStrategy.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 6045bdf023..30c0cd45dd 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -48,9 +48,9 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); - Optional isDisabled = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields); - if (isDisabled.isPresent()) { - return CompletableFuture.completedFuture(isDisabled.get()); + Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields); + if (isNotSensible.isPresent()) { + return CompletableFuture.completedFuture(isNotSensible.get()); } DeferredExecutionSupport deferredExecutionSupport = createDeferredExecutionSupport(executionContext, parameters); diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index 6dbff81130..3938c54d75 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -44,9 +44,9 @@ public CompletableFuture execute(ExecutionContext executionCont // this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code // so belts and braces - Optional isDisabled = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields); - if (isDisabled.isPresent()) { - return CompletableFuture.completedFuture(isDisabled.get()); + Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields); + if (isNotSensible.isPresent()) { + return CompletableFuture.completedFuture(isNotSensible.get()); } CompletableFuture> resultsFuture = Async.eachSequentially(fieldNames, (fieldName, prevResults) -> { From c032d57f48415e89df8aebb6502ac99a5fba0aac Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sun, 17 Mar 2024 17:14:04 +1100 Subject: [PATCH 326/393] This moves the execution stopping into the ES - extra test for AsyncSerialExecutionStrategy --- .../introspection/IntrospectionTest.groovy | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy index 9c6abe3c91..a23e6531a2 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy @@ -3,6 +3,7 @@ package graphql.introspection import graphql.ErrorType import graphql.ExecutionInput import graphql.TestUtil +import graphql.execution.AsyncSerialExecutionStrategy import graphql.schema.DataFetcher import graphql.schema.FieldCoordinates import graphql.schema.GraphQLCodeRegistry @@ -548,7 +549,7 @@ class IntrospectionTest extends Specification { then: def oldQuery = oldIntrospectionQuery.replaceAll("\\s+", "") - def newQuery = newIntrospectionQuery.replaceAll("\\s+","") + def newQuery = newIntrospectionQuery.replaceAll("\\s+", "") oldQuery == newQuery } @@ -743,4 +744,31 @@ class IntrospectionTest extends Specification { er.errors[0] instanceof IntrospectionDisabledError er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled } + + def "AsyncSerialExecutionStrategy with jvm wide enablement"() { + def graphQL = TestUtil.graphQL("type Query { f : String } ") + .queryExecutionStrategy(new AsyncSerialExecutionStrategy()).build() + + when: + def er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + + when: + Introspection.enabledJvmWide(false) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + + when: + Introspection.enabledJvmWide(true) + er = graphQL.execute(IntrospectionQuery.INTROSPECTION_QUERY) + + then: + er.errors.isEmpty() + } + } From 9b42c1f746ba1dd4840e67f3b3b45ee76df70faa Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 18 Mar 2024 11:58:42 +1000 Subject: [PATCH 327/393] introduce ResultNodesInfo class --- .../java/graphql/execution/Execution.java | 1 + .../graphql/execution/ExecutionContext.java | 8 +-- .../graphql/execution/ExecutionStrategy.java | 14 +++-- .../graphql/execution/ResultNodesInfo.java | 54 +++++++++++++++++++ src/test/groovy/graphql/GraphQLTest.groovy | 33 ++++++++---- 5 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 src/main/java/graphql/execution/ResultNodesInfo.java diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 3a30bc1062..82f7330155 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -107,6 +107,7 @@ public CompletableFuture execute(Document document, GraphQLSche .executionInput(executionInput) .build(); + executionContext.getGraphQLContext().put(ResultNodesInfo.RESULT_NODES_INFO, executionContext.getResultNodesInfo()); InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters( executionInput, graphQLSchema, instrumentationState diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 935965f915..9cd45a4e30 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -27,7 +27,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; @@ -60,11 +59,12 @@ public class ExecutionContext { private final ValueUnboxer valueUnboxer; private final ExecutionInput executionInput; private final Supplier queryTree; - private final AtomicInteger resultNodesCount = new AtomicInteger(); // this is modified after creation so it needs to be volatile to ensure visibility across Threads private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP; + private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); + ExecutionContext(ExecutionContextBuilder builder) { this.graphQLSchema = builder.graphQLSchema; this.executionId = builder.executionId; @@ -307,7 +307,7 @@ public ExecutionContext transform(Consumer builderConsu return builder.build(); } - public AtomicInteger getResultNodesCount() { - return resultNodesCount; + public ResultNodesInfo getResultNodesInfo() { + return resultNodesInfo; } } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index b24c4cfe35..2af802a884 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -67,6 +67,7 @@ import static graphql.execution.FieldValueInfo.CompleteValueType.NULL; import static graphql.execution.FieldValueInfo.CompleteValueType.OBJECT; import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR; +import static graphql.execution.ResultNodesInfo.MAX_RESULT_NODES; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment; import static graphql.schema.GraphQLTypeUtil.isEnum; @@ -131,10 +132,6 @@ @SuppressWarnings("FutureReturnValueIgnored") public abstract class ExecutionStrategy { - @Internal - public static final String MAX_RESULT_NODES = "__MAX_RESULT_NODES"; - @Internal - public static final String MAX_RESULT_NODES_BREACHED = "__MAX_RESULT_NODES_BREACHED"; protected final FieldCollector fieldCollector = new FieldCollector(); protected final ExecutionStepInfoFactory executionStepInfoFactory = new ExecutionStepInfoFactory(); protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; @@ -386,11 +383,12 @@ protected CompletableFuture fetchField(ExecutionContext executionC private CompletableFuture fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - int resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); + int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount(); + Integer maxNodes; if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { if (resultNodesCount > maxNodes) { - executionContext.getGraphQLContext().put(MAX_RESULT_NODES_BREACHED, true); + executionContext.getResultNodesInfo().maxResultNodesExceeded(); return CompletableFuture.completedFuture(new FetchedValue(null, Collections.emptyList(), null)); } } @@ -726,11 +724,11 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, List fieldValueInfos = new ArrayList<>(size.orElse(1)); int index = 0; for (Object item : iterableValues) { - int resultNodesCount = executionContext.getResultNodesCount().incrementAndGet(); + int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount(); Integer maxNodes; if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { if (resultNodesCount > maxNodes) { - executionContext.getGraphQLContext().put(MAX_RESULT_NODES_BREACHED, true); + executionContext.getResultNodesInfo().maxResultNodesExceeded(); return new FieldValueInfo(NULL, completedFuture(null), fieldValueInfos); } } diff --git a/src/main/java/graphql/execution/ResultNodesInfo.java b/src/main/java/graphql/execution/ResultNodesInfo.java new file mode 100644 index 0000000000..765a46437b --- /dev/null +++ b/src/main/java/graphql/execution/ResultNodesInfo.java @@ -0,0 +1,54 @@ +package graphql.execution; + +import graphql.Internal; +import graphql.PublicApi; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class is used to track the number of result nodes that have been created during execution. + * After each execution the GraphQLContext contains a ResultNodeInfo object under the key {@link ResultNodesInfo#RESULT_NODES_INFO} + *

+ * The number of result can be limited (and should be for security reasons) by setting the maximum number of result nodes + * in the GraphQLContext under the key {@link ResultNodesInfo#MAX_RESULT_NODES} + */ +@PublicApi +public class ResultNodesInfo { + + public static final String MAX_RESULT_NODES = "__MAX_RESULT_NODES"; + public static final String RESULT_NODES_INFO = "__RESULT_NODES_INFO"; + + private volatile boolean maxResultNodesExceeded = false; + private final AtomicInteger resultNodesCount = new AtomicInteger(0); + + @Internal + public int incrementAndGetResultNodesCount() { + return resultNodesCount.incrementAndGet(); + } + + @Internal + public void maxResultNodesExceeded() { + this.maxResultNodesExceeded = true; + } + + /** + * The number of result nodes created. + * Note: this can be higher than max result nodes because + * a each node that exceeds the number of max nodes is set to null, + * but still is a result node (which value null) + * + * @return + */ + public int getResultNodesCount() { + return resultNodesCount.get(); + } + + /** + * If the number of result nodes has exceeded the maximum allowed numbers. + * + * @return + */ + public boolean isMaxResultNodesExceeded() { + return maxResultNodesExceeded; + } +} diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index 4bb75d0b76..8aa767afeb 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -11,9 +11,9 @@ import graphql.execution.DataFetcherResult import graphql.execution.ExecutionContext import graphql.execution.ExecutionId import graphql.execution.ExecutionIdProvider -import graphql.execution.ExecutionStrategy import graphql.execution.ExecutionStrategyParameters import graphql.execution.MissingRootTypeException +import graphql.execution.ResultNodesInfo import graphql.execution.SubscriptionExecutionStrategy import graphql.execution.ValueUnboxer import graphql.execution.instrumentation.Instrumentation @@ -50,6 +50,7 @@ import static graphql.ExecutionInput.Builder import static graphql.ExecutionInput.newExecutionInput import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString +import static graphql.execution.ResultNodesInfo.MAX_RESULT_NODES import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition import static graphql.schema.GraphQLInputObjectField.newInputObjectField @@ -1444,12 +1445,14 @@ many lines'''] def query = "{ hello h1: hello h2: hello h3: hello } " def ei = newExecutionInput(query).build() - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 4); + ei.getGraphQLContext().put(MAX_RESULT_NODES, 4); when: def er = graphQL.execute(ei) + def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo then: - ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == null + !rni.maxResultNodesExceeded + rni.resultNodesCount == 4 er.data == [hello: "world", h1: "world", h2: "world", h3: "world"] } @@ -1468,12 +1471,14 @@ many lines'''] def query = "{ hello h1: hello h2: hello h3: hello } " def ei = newExecutionInput(query).build() - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3); + ei.getGraphQLContext().put(MAX_RESULT_NODES, 3); when: def er = graphQL.execute(ei) + def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo then: - ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == true + rni.maxResultNodesExceeded + rni.resultNodesCount == 4 er.data == [hello: "world", h1: "world", h2: "world", h3: null] } @@ -1492,12 +1497,14 @@ many lines'''] def query = "{ hello}" def ei = newExecutionInput(query).build() - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 3); + ei.getGraphQLContext().put(MAX_RESULT_NODES, 3); when: def er = graphQL.execute(ei) + def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo then: - ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == true + rni.maxResultNodesExceeded + rni.resultNodesCount == 4 er.data == [hello: null] } @@ -1520,12 +1527,14 @@ many lines'''] def query = "{ hello {name}}" def ei = newExecutionInput(query).build() // we have 7 result nodes overall - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 6); + ei.getGraphQLContext().put(MAX_RESULT_NODES, 6); when: def er = graphQL.execute(ei) + def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo then: - ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == true + rni.resultNodesCount == 7 + rni.maxResultNodesExceeded er.data == [hello: [[name: "w1"], [name: "w2"], [name: null]]] } @@ -1548,12 +1557,14 @@ many lines'''] def query = "{ hello {name}}" def ei = newExecutionInput(query).build() // we have 7 result nodes overall - ei.getGraphQLContext().put(ExecutionStrategy.MAX_RESULT_NODES, 7); + ei.getGraphQLContext().put(MAX_RESULT_NODES, 7); when: def er = graphQL.execute(ei) + def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo then: - ei.getGraphQLContext().get(ExecutionStrategy.MAX_RESULT_NODES_BREACHED) == null + !rni.maxResultNodesExceeded + rni.resultNodesCount == 7 er.data == [hello: [[name: "w1"], [name: "w2"], [name: "w3"]]] } From 24dbfe9e828a78b840a6b5b9e955b2526b8ff2dd Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 18 Mar 2024 12:01:45 +1000 Subject: [PATCH 328/393] javadoc --- src/main/java/graphql/execution/ResultNodesInfo.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/ResultNodesInfo.java b/src/main/java/graphql/execution/ResultNodesInfo.java index 765a46437b..0914954fc5 100644 --- a/src/main/java/graphql/execution/ResultNodesInfo.java +++ b/src/main/java/graphql/execution/ResultNodesInfo.java @@ -8,9 +8,10 @@ /** * This class is used to track the number of result nodes that have been created during execution. * After each execution the GraphQLContext contains a ResultNodeInfo object under the key {@link ResultNodesInfo#RESULT_NODES_INFO} - *

+ *

* The number of result can be limited (and should be for security reasons) by setting the maximum number of result nodes * in the GraphQLContext under the key {@link ResultNodesInfo#MAX_RESULT_NODES} + *

*/ @PublicApi public class ResultNodesInfo { @@ -37,7 +38,7 @@ public void maxResultNodesExceeded() { * a each node that exceeds the number of max nodes is set to null, * but still is a result node (which value null) * - * @return + * @return number of result nodes created */ public int getResultNodesCount() { return resultNodesCount.get(); @@ -46,7 +47,7 @@ public int getResultNodesCount() { /** * If the number of result nodes has exceeded the maximum allowed numbers. * - * @return + * @return true if the number of result nodes has exceeded the maximum allowed numbers */ public boolean isMaxResultNodesExceeded() { return maxResultNodesExceeded; From 7e5b45b5068a57de3e76df7954b6c1f481063ac9 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 18 Mar 2024 13:28:16 +1100 Subject: [PATCH 329/393] GoodFaithIntrospection is now on by defaut and always a available - no longer an instrumentation --- .../execution/AsyncExecutionStrategy.java | 2 +- .../AsyncSerialExecutionStrategy.java | 2 +- .../introspection/GoodFaithIntrospection.java | 122 +++++++++++++++ ...GoodFaithIntrospectionInstrumentation.java | 148 ------------------ .../graphql/introspection/Introspection.java | 10 +- ...ithIntrospectionInstrumentationTest.groovy | 99 +++++------- 6 files changed, 172 insertions(+), 211 deletions(-) create mode 100644 src/main/java/graphql/introspection/GoodFaithIntrospection.java delete mode 100644 src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index 30c0cd45dd..1e7bc7c79f 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -48,7 +48,7 @@ public CompletableFuture execute(ExecutionContext executionCont MergedSelectionSet fields = parameters.getFields(); List fieldNames = fields.getKeys(); - Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(),fields); + Optional isNotSensible = Introspection.isIntrospectionSensible(fields, executionContext); if (isNotSensible.isPresent()) { return CompletableFuture.completedFuture(isNotSensible.get()); } diff --git a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java index 3938c54d75..6f64b8cd8c 100644 --- a/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncSerialExecutionStrategy.java @@ -44,7 +44,7 @@ public CompletableFuture execute(ExecutionContext executionCont // this is highly unlikely since Mutations cant do introspection BUT in theory someone could make the query strategy this code // so belts and braces - Optional isNotSensible = Introspection.isIntrospectionSensible(executionContext.getGraphQLContext(), fields); + Optional isNotSensible = Introspection.isIntrospectionSensible(fields, executionContext); if (isNotSensible.isPresent()) { return CompletableFuture.completedFuture(isNotSensible.get()); } diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospection.java b/src/main/java/graphql/introspection/GoodFaithIntrospection.java new file mode 100644 index 0000000000..7e853016ac --- /dev/null +++ b/src/main/java/graphql/introspection/GoodFaithIntrospection.java @@ -0,0 +1,122 @@ +package graphql.introspection; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import graphql.ErrorClassification; +import graphql.ExecutionResult; +import graphql.GraphQLContext; +import graphql.GraphQLError; +import graphql.PublicApi; +import graphql.execution.ExecutionContext; +import graphql.language.SourceLocation; +import graphql.normalized.ExecutableNormalizedField; +import graphql.normalized.ExecutableNormalizedOperation; +import graphql.schema.FieldCoordinates; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import static graphql.schema.FieldCoordinates.coordinates; + +/** + * This {@link graphql.execution.instrumentation.Instrumentation} ensure that a submitted introspection query is done in + * good faith. + *

+ * There are attack vectors where a crafted introspection query can cause the engine to spend too much time + * producing introspection data. This is especially true on large schemas with lots of types and fields. + *

+ * Schemas form a cyclic graph and hence it's possible to send in introspection queries that can reference those cycles + * and in large schemas this can be expensive and perhaps a "denial of service". + *

+ * This instrumentation only allows one __schema field or one __type field to be present, and it does not allow the `__Type` fields + * to form a cycle, i.e., that can only be present once. This allows the standard and common introspection queries to work + * so tooling such as graphiql can work. + */ +@PublicApi +public class GoodFaithIntrospection { + + /** + * Placing a boolean value under this key in the per request {@link GraphQLContext} will enable + * or disable Good Faith Introspection on that request. + */ + public static final String GOOD_FAITH_INTROSPECTION_DISABLED = "GOOD_FAITH_INTROSPECTION_DISABLED"; + + private static final AtomicBoolean ENABLED_STATE = new AtomicBoolean(true); + + /** + * @return true if good faith introspection is enabled + */ + public static boolean isEnabledJvmWide() { + return ENABLED_STATE.get(); + } + + /** + * This allows you to disable good faith introspection, which is on by default. + * + * @param flag the desired state + * + * @return the previous state + */ + public static boolean enabledJvmWide(boolean flag) { + return ENABLED_STATE.getAndSet(flag); + } + + private static final Map ALLOWED_FIELD_INSTANCES = Map.of( + coordinates("Query", "__schema"), 1 + , coordinates("Query", "__type"), 1 + + , coordinates("__Type", "fields"), 1 + , coordinates("__Type", "inputFields"), 1 + , coordinates("__Type", "interfaces"), 1 + , coordinates("__Type", "possibleTypes"), 1 + ); + + public static Optional checkIntrospection(ExecutionContext executionContext) { + if (isIntrospectionEnabled(executionContext.getGraphQLContext())) { + ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get(); + ImmutableListMultimap coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); + for (Map.Entry entry : ALLOWED_FIELD_INSTANCES.entrySet()) { + FieldCoordinates coordinates = entry.getKey(); + Integer allowSize = entry.getValue(); + ImmutableList normalizedFields = coordinatesToENFs.get(coordinates); + if (normalizedFields.size() > allowSize) { + BadFaithIntrospectionError error = new BadFaithIntrospectionError(coordinates.toString()); + return Optional.of(ExecutionResult.newExecutionResult().addError(error).build()); + } + } + } + return Optional.empty(); + } + + private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { + if (!isEnabledJvmWide()) { + return false; + } + return !graphQlContext.getOrDefault(GOOD_FAITH_INTROSPECTION_DISABLED, false); + } + + public static class BadFaithIntrospectionError implements GraphQLError { + private final String message; + + public BadFaithIntrospectionError(String qualifiedField) { + this.message = String.format("This request is not asking for introspection in good faith - %s is present too often!", qualifiedField); + } + + @Override + public String getMessage() { + return message; + } + + @Override + public ErrorClassification getErrorType() { + return ErrorClassification.errorClassification("BadFaithIntrospection"); + } + + @Override + public List getLocations() { + return null; + } + } +} diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java b/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java deleted file mode 100644 index d95567cd9c..0000000000 --- a/src/main/java/graphql/introspection/GoodFaithIntrospectionInstrumentation.java +++ /dev/null @@ -1,148 +0,0 @@ -package graphql.introspection; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; -import graphql.PublicApi; -import graphql.execution.AbortExecutionException; -import graphql.execution.ExecutionContext; -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.SimplePerformantInstrumentation; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import graphql.normalized.ExecutableNormalizedField; -import graphql.normalized.ExecutableNormalizedOperation; -import graphql.schema.FieldCoordinates; -import org.jetbrains.annotations.Nullable; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static graphql.schema.FieldCoordinates.coordinates; - -/** - * This {@link graphql.execution.instrumentation.Instrumentation} ensure that a submitted introspection query is done in - * good faith. - *

- * There are attack vectors where a crafted introspection query can cause the engine to spend too much time - * producing introspection data. This is especially true on large schemas with lots of types and fields. - *

- * Schemas form a cyclic graph and hence it's possible to send in introspection queries that can reference those cycles - * and in large schemas this can be expensive and perhaps a "denial of service". - *

- * This instrumentation only allows one __schema field or one __type field to be present, and it does not allow the `__Type` fields - * to form a cycle, i.e., that can only be present once. This allows the standard and common introspection queries to work - * so tooling such as graphiql can work. - */ -@PublicApi -public class GoodFaithIntrospectionInstrumentation extends SimplePerformantInstrumentation { - - private static final Map ALLOWED_FIELD_INSTANCES = Map.of( - coordinates("Query", "__schema"), 1 - , coordinates("Query", "__type"), 1 - - , coordinates("__Type", "fields"), 1 - , coordinates("__Type", "inputFields"), 1 - , coordinates("__Type", "interfaces"), 1 - , coordinates("__Type", "possibleTypes"), 1 - ); - private final Map allowFieldInstances; - - public GoodFaithIntrospectionInstrumentation() { - this(ALLOWED_FIELD_INSTANCES); - } - - private GoodFaithIntrospectionInstrumentation(Map allowFieldInstances) { - this.allowFieldInstances = allowFieldInstances; - } - - @Override - public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { - - // We check first to see if there are any introspection fields present on the top level field set - // if there isn't, then there is nothing to check. This ensures we are performant. - List topLevelFieldName = parameters.getExecutionStrategyParameters().getFields().getKeys(); - boolean weHaveIntrospectionFields = false; - for (String fieldName : topLevelFieldName) { - if (Introspection.SchemaMetaFieldDef.getName().equals(fieldName) || Introspection.TypeMetaFieldDef.getName().equals(fieldName)) { - weHaveIntrospectionFields = true; - break; - } - } - if (weHaveIntrospectionFields) { - ensureTheyAreInGoodFaith(parameters.getExecutionContext()); - } - return ExecutionStrategyInstrumentationContext.NOOP; - } - - private void ensureTheyAreInGoodFaith(ExecutionContext executionContext) { - ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get(); - ImmutableListMultimap coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); - for (Map.Entry entry : allowFieldInstances.entrySet()) { - FieldCoordinates coordinates = entry.getKey(); - Integer allowSize = entry.getValue(); - ImmutableList normalizedFields = coordinatesToENFs.get(coordinates); - if (normalizedFields.size() > allowSize) { - throw new BadFaithIntrospectionAbortExecutionException(coordinates.toString()); - } - } - } - - @PublicApi - public static class BadFaithIntrospectionAbortExecutionException extends AbortExecutionException { - public BadFaithIntrospectionAbortExecutionException(String qualifiedField) { - super(String.format("This request is not asking for introspection in good faith - %s is present too often!", qualifiedField)); - } - } - - public static Builder newGoodFaithIntrospection() { - return new Builder(); - } - - public static class Builder { - - - private final Map allowFieldInstances = new LinkedHashMap<>(ALLOWED_FIELD_INSTANCES); - - /** - * This allows you to set how many __type(name : "x) field instances are allowed in an introspection query - * - * @param maxInstances the number allowed - * - * @return this builder - */ - public Builder maximumUnderscoreTypeInstances(int maxInstances) { - allowFieldInstances.put(coordinates("Query", "__type"), maxInstances); - return this; - } - - /** - * This allows you to set how many __schema field instances are allowed in an introspection query - * - * @param maxInstances the number allowed - * - * @return this builder - */ - public Builder maximumUnderscoreSchemaInstances(int maxInstances) { - allowFieldInstances.put(coordinates("Query", "__schema"), maxInstances); - return this; - } - - /** - * This allows you to set how many qualified field instances are allowed in an introspection query - * - * @param coordinates - the qualified field name - * @param maxInstances the number allowed - * - * @return this builder - */ - public Builder maximumFieldInstances(FieldCoordinates coordinates, int maxInstances) { - allowFieldInstances.put(coordinates, maxInstances); - return this; - } - - public GoodFaithIntrospectionInstrumentation build() { - return new GoodFaithIntrospectionInstrumentation(allowFieldInstances); - } - } -} diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 4015ff393b..c228f1c39f 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -7,6 +7,7 @@ import graphql.GraphQLContext; import graphql.Internal; import graphql.PublicApi; +import graphql.execution.ExecutionContext; import graphql.execution.MergedField; import graphql.execution.MergedSelectionSet; import graphql.execution.ValuesResolver; @@ -108,10 +109,12 @@ public static boolean isEnabledJvmWide() { * that can be returned to the user. * * @param mergedSelectionSet the fields to be executed + * @param executionContext the execution context in play * * @return an optional error result */ - public static Optional isIntrospectionSensible(GraphQLContext graphQLContext, MergedSelectionSet mergedSelectionSet) { + public static Optional isIntrospectionSensible(MergedSelectionSet mergedSelectionSet, ExecutionContext executionContext) { + GraphQLContext graphQLContext = executionContext.getGraphQLContext(); MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName()); if (schemaField != null) { if (!isIntrospectionEnabled(graphQLContext)) { @@ -124,7 +127,10 @@ public static Optional isIntrospectionSensible(GraphQLContext g return mkDisabledError(typeField); } } - // later we can put a good faith check code here to check the fields make sense + if (schemaField != null || typeField != null) + { + return GoodFaithIntrospection.checkIntrospection(executionContext); + } return Optional.empty(); } diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy index 1450aa456c..969e0abd60 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -1,13 +1,17 @@ package graphql.introspection +import graphql.ExecutionInput import graphql.ExecutionResult import graphql.TestUtil -import graphql.schema.FieldCoordinates import spock.lang.Specification class GoodFaithIntrospectionInstrumentationTest extends Specification { - def graphql = TestUtil.graphQL("type Query { f : String }").instrumentation(new GoodFaithIntrospectionInstrumentation()).build() + def graphql = TestUtil.graphQL("type Query { normalField : String }").build() + + def cleanup() { + GoodFaithIntrospection.enabledJvmWide(true) + } def "test asking for introspection in good faith"() { @@ -23,7 +27,7 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { ExecutionResult er = graphql.execute(query) then: !er.errors.isEmpty() - er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + er.errors[0] instanceof GoodFaithIntrospection.BadFaithIntrospectionError where: query | _ @@ -70,83 +74,60 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { """ | _ } - def "test builder"() { - GoodFaithIntrospectionInstrumentation goodFaithIntrospectionInstrumentation = GoodFaithIntrospectionInstrumentation.newGoodFaithIntrospection() - .maximumUnderscoreSchemaInstances(2) - .maximumUnderscoreTypeInstances(3) - .maximumFieldInstances(FieldCoordinates.coordinates("__Type", "fields"), 2) - .build() - - def graphql = TestUtil.graphQL("type Query { f : String }").instrumentation(goodFaithIntrospectionInstrumentation).build() + def "mixed general queries and introspections will be stopped anyway"() { + def query = """ + query goodAndBad { + normalField + __schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}} + } + """ when: - def er = graphql.execute(IntrospectionQuery.INTROSPECTION_QUERY) + ExecutionResult er = graphql.execute(query) then: - er.errors.isEmpty() + !er.errors.isEmpty() + er.errors[0] instanceof GoodFaithIntrospection.BadFaithIntrospectionError + er.data == null // it stopped hard - it did not continue to normal business + } + def "can be disabled"() { when: - er = graphql.execute(""" - query ok { - __schema { types { name } } - alias1: __schema { types { name } } - } - """) + def currentState = GoodFaithIntrospection.isEnabledJvmWide() + then: - er.errors.isEmpty() + currentState when: - er = graphql.execute(""" - query ok { - __schema { types { name } } - alias1: __schema { types { name } } - alias2: __schema { types { name } } - } - """) + def prevState = GoodFaithIntrospection.enabledJvmWide(false) + then: - !er.errors.isEmpty() - er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + prevState when: - er = graphql.execute(""" - query ok { - __type(name : "Query") { name } - alias1 : __type(name : "Query") { name } - alias2 : __type(name : "Query") { name } - } - """) + ExecutionResult er = graphql.execute("query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}}") + then: er.errors.isEmpty() + } + def "can be disabled per request"() { when: - er = graphql.execute(""" - query ok { - __type(name : "Query") { name } - alias1 : __type(name : "Query") { name } - alias2 : __type(name : "Query") { name } - alias3 : __type(name : "Query") { name } - } - """) - then: - !er.errors.isEmpty() - er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + def context = [(GoodFaithIntrospection.GOOD_FAITH_INTROSPECTION_DISABLED): true] + ExecutionInput executionInput = ExecutionInput.newExecutionInput("query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}}") + .graphQLContext(context).build() + ExecutionResult er = graphql.execute(executionInput) - when: - er = graphql.execute(""" - query ok { - __schema { types { fields { type { fields { name }}}}} - } - """) then: er.errors.isEmpty() when: - er = graphql.execute(""" - query ok { - __schema { types { fields { type { fields { type { fields { name }}}}}}} - } - """) + context = [(GoodFaithIntrospection.GOOD_FAITH_INTROSPECTION_DISABLED): false] + executionInput = ExecutionInput.newExecutionInput("query badActor{__schema{types{fields{type{fields{type{fields{type{fields{type{name}}}}}}}}}}}") + .graphQLContext(context).build() + er = graphql.execute(executionInput) + then: !er.errors.isEmpty() - er.errors[0] instanceof GoodFaithIntrospectionInstrumentation.BadFaithIntrospectionAbortExecutionException + er.errors[0] instanceof GoodFaithIntrospection.BadFaithIntrospectionError } } From db1b6cc5ca72aaa9a17aac4fb6dbc85393d14741 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 18 Mar 2024 13:34:20 +1100 Subject: [PATCH 330/393] Extra test to show mixed queries stop hard --- .../introspection/IntrospectionTest.groovy | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy index a23e6531a2..da5b24092d 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy @@ -745,6 +745,32 @@ class IntrospectionTest extends Specification { er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled } + def "mixed schema and other fields stop early"() { + def graphQL = TestUtil.graphQL("type Query { normalField : String } ").build() + + def query = """ + query goodAndBad { + normalField + __schema{ types{ fields { name }}} + } + """ + + when: + def er = graphQL.execute(query) + + then: + er.errors.isEmpty() + + when: + Introspection.enabledJvmWide(false) + er = graphQL.execute(query) + + then: + er.errors[0] instanceof IntrospectionDisabledError + er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + er.data == null // stops hard + } + def "AsyncSerialExecutionStrategy with jvm wide enablement"() { def graphQL = TestUtil.graphQL("type Query { f : String } ") .queryExecutionStrategy(new AsyncSerialExecutionStrategy()).build() From 232b9a89e495efdeb71bee115b87d998d4fa6d6e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 18 Mar 2024 13:51:28 +1100 Subject: [PATCH 331/393] reverting error type --- src/main/java/graphql/ErrorType.java | 1 - .../java/graphql/introspection/IntrospectionDisabledError.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/graphql/ErrorType.java b/src/main/java/graphql/ErrorType.java index f9f01582dc..9adee6d461 100644 --- a/src/main/java/graphql/ErrorType.java +++ b/src/main/java/graphql/ErrorType.java @@ -11,6 +11,5 @@ public enum ErrorType implements ErrorClassification { DataFetchingException, NullValueInNonNullableField, OperationNotSupported, - IntrospectionDisabled, ExecutionAborted } diff --git a/src/main/java/graphql/introspection/IntrospectionDisabledError.java b/src/main/java/graphql/introspection/IntrospectionDisabledError.java index d48a8cca05..cbd7e077a3 100644 --- a/src/main/java/graphql/introspection/IntrospectionDisabledError.java +++ b/src/main/java/graphql/introspection/IntrospectionDisabledError.java @@ -30,6 +30,6 @@ public List getLocations() { @Override public ErrorClassification getErrorType() { - return ErrorType.IntrospectionDisabled; + return ErrorClassification.errorClassification("IntrospectionDisabled"); } } From 80fcbb96be990b528bec306c9a9c2dc078b895fa Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 18 Mar 2024 13:33:53 +1000 Subject: [PATCH 332/393] javadoc --- src/main/java/graphql/execution/ResultNodesInfo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/execution/ResultNodesInfo.java b/src/main/java/graphql/execution/ResultNodesInfo.java index 0914954fc5..609860fc6a 100644 --- a/src/main/java/graphql/execution/ResultNodesInfo.java +++ b/src/main/java/graphql/execution/ResultNodesInfo.java @@ -10,7 +10,7 @@ * After each execution the GraphQLContext contains a ResultNodeInfo object under the key {@link ResultNodesInfo#RESULT_NODES_INFO} *

* The number of result can be limited (and should be for security reasons) by setting the maximum number of result nodes - * in the GraphQLContext under the key {@link ResultNodesInfo#MAX_RESULT_NODES} + * in the GraphQLContext under the key {@link ResultNodesInfo#MAX_RESULT_NODES} of type Integer. *

*/ @PublicApi @@ -36,7 +36,7 @@ public void maxResultNodesExceeded() { * The number of result nodes created. * Note: this can be higher than max result nodes because * a each node that exceeds the number of max nodes is set to null, - * but still is a result node (which value null) + * but still is a result node (with value null) * * @return number of result nodes created */ From c8f3c08ea856b0874db9f20afa72c50c1a72f540 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 18 Mar 2024 13:34:16 +1000 Subject: [PATCH 333/393] javadoc --- src/main/java/graphql/execution/ResultNodesInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/ResultNodesInfo.java b/src/main/java/graphql/execution/ResultNodesInfo.java index 609860fc6a..afc366f6be 100644 --- a/src/main/java/graphql/execution/ResultNodesInfo.java +++ b/src/main/java/graphql/execution/ResultNodesInfo.java @@ -10,7 +10,7 @@ * After each execution the GraphQLContext contains a ResultNodeInfo object under the key {@link ResultNodesInfo#RESULT_NODES_INFO} *

* The number of result can be limited (and should be for security reasons) by setting the maximum number of result nodes - * in the GraphQLContext under the key {@link ResultNodesInfo#MAX_RESULT_NODES} of type Integer. + * in the GraphQLContext under the key {@link ResultNodesInfo#MAX_RESULT_NODES} to an Integer *

*/ @PublicApi From 498175a21bdbf93aec87b7a715a8d9b2d715fb1e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 18 Mar 2024 14:53:07 +1100 Subject: [PATCH 334/393] fixing tests --- .../introspection/IntrospectionTest.groovy | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy index da5b24092d..8a70c68618 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy @@ -1,6 +1,5 @@ package graphql.introspection -import graphql.ErrorType import graphql.ExecutionInput import graphql.TestUtil import graphql.execution.AsyncSerialExecutionStrategy @@ -24,6 +23,14 @@ import static graphql.schema.GraphQLSchema.newSchema class IntrospectionTest extends Specification { + def setup() { + Introspection.enabledJvmWide(true) + } + + def cleanup() { + Introspection.enabledJvmWide(true) + } + def "bug 1186 - introspection depth check"() { def spec = ''' type Query { @@ -705,7 +712,7 @@ class IntrospectionTest extends Specification { then: er.errors[0] instanceof IntrospectionDisabledError - er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" when: Introspection.enabledJvmWide(true) @@ -742,7 +749,7 @@ class IntrospectionTest extends Specification { then: er.errors[0] instanceof IntrospectionDisabledError - er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" } def "mixed schema and other fields stop early"() { @@ -767,7 +774,7 @@ class IntrospectionTest extends Specification { then: er.errors[0] instanceof IntrospectionDisabledError - er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" er.data == null // stops hard } @@ -787,7 +794,7 @@ class IntrospectionTest extends Specification { then: er.errors[0] instanceof IntrospectionDisabledError - er.errors[0].getErrorType() == ErrorType.IntrospectionDisabled + er.errors[0].getErrorType().toString() == "IntrospectionDisabled" when: Introspection.enabledJvmWide(true) From e51361bfc22e61dbcb262c04000039822a87852d Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 18 Mar 2024 17:20:55 +1100 Subject: [PATCH 335/393] fixing tests - for one at a time --- ...ithIntrospectionInstrumentationTest.groovy | 3 ++ ...ormalizedOperationToAstCompilerTest.groovy | 48 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy index 969e0abd60..f1ffc2c570 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -9,6 +9,9 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { def graphql = TestUtil.graphQL("type Query { normalField : String }").build() + def setup() { + GoodFaithIntrospection.enabledJvmWide(true) + } def cleanup() { GoodFaithIntrospection.enabledJvmWide(true) } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 27c4c89a6d..a135ac3f9b 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -1387,17 +1387,10 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat ''' } - def "introspection query can be printed"() { + def "introspection query can be printed __schema"() { def sdl = ''' type Query { - foo1: Foo - } - interface Foo { - test: String - } - type AFoo implements Foo { - test: String - aFoo: String + f: String } ''' def query = ''' @@ -1409,14 +1402,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat } } } - - __type(name: "World") { - name - fields { - name - } - } - } + } ''' GraphQLSchema schema = mkSchema(sdl) @@ -1433,6 +1419,34 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat } } } +} +''' + } + + def "introspection query can be printed __type"() { + def sdl = ''' + type Query { + f: String + } + ''' + def query = ''' + query introspection_query { + __type(name: "World") { + name + fields { + name + } + } + } + ''' + + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) + def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + documentPrinted == '''{ __type(name: "World") { fields { name From 1f95f69115c56d706d99e4e854854808d7cedcf1 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 18 Mar 2024 20:47:01 +1000 Subject: [PATCH 336/393] adding a cycle schema analyzer --- .../graphql/util/CyclicSchemaAnalyzer.java | 89 ++++++++++++++ .../util/CyclicSchemaAnalyzerTest.groovy | 112 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/main/java/graphql/util/CyclicSchemaAnalyzer.java create mode 100644 src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy diff --git a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java new file mode 100644 index 0000000000..6b9858059d --- /dev/null +++ b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java @@ -0,0 +1,89 @@ +package graphql.util; + +import graphql.ExperimentalApi; +import graphql.introspection.Introspection; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeUtil; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.SchemaTraverser; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +/** + * Finds all cycles in a GraphQL Schema. + * Cycles caused by built-in introspection types are filtered out. + */ +@ExperimentalApi +public class CyclicSchemaAnalyzer { + + public static class SchemaElementWithChild { + private final GraphQLSchemaElement element; + private final int childIndex; + + public SchemaElementWithChild(GraphQLSchemaElement element, int childIndex) { + this.element = element; + this.childIndex = childIndex; + } + } + + public static class SchemaCycle { + private final List cycleElements = new ArrayList<>(); + + public String toString() { + StringJoiner result = new StringJoiner(" -> "); + for (SchemaElementWithChild schemaElementWithChild : cycleElements) { + result.add(GraphQLTypeUtil.simplePrint(schemaElementWithChild.element) + "/" + schemaElementWithChild.childIndex); + } + return result.toString(); + } + + public int size() { + return cycleElements.size(); + } + } + + public static List findCycles(GraphQLSchema schema) { + SchemaTraverser traverser = new SchemaTraverser(); + Visitor visitor = new Visitor(); + traverser.depthFirstFullSchema(visitor, schema); + List result = new ArrayList<>(); + for (List> possibleCycle : visitor.cycles) { + SchemaCycle schemaCycle = new SchemaCycle(); + // the breadcrumbs are in the reverse order of the dependency + for (int i = possibleCycle.size() - 1; i >= 0; i--) { + Breadcrumb breadcrumb = possibleCycle.get(i); + SchemaElementWithChild schemaElementWithChild = new SchemaElementWithChild(breadcrumb.getNode(), breadcrumb.getLocation().getIndex()); + schemaCycle.cycleElements.add(schemaElementWithChild); + } + result.add(schemaCycle); + } + return result; + } + + private static class Visitor extends GraphQLTypeVisitorStub { + public final List>> cycles = new ArrayList<>(); + + @Override + public TraversalControl visitBackRef(TraverserContext context) { + List> breadcrumbs = context.getBreadcrumbs(); + + for (int i = breadcrumbs.size() - 1; i >= 0; i--) { + Breadcrumb breadcrumb = breadcrumbs.get(i); + if (breadcrumb.getNode() instanceof GraphQLNamedType && Introspection.isIntrospectionTypes((GraphQLNamedType) breadcrumb.getNode())) { + return TraversalControl.CONTINUE; + } + if (breadcrumb.getNode() == context.thisNode()) { + List> cycle = breadcrumbs.subList(0, i + 1); + cycles.add(cycle); + } + } + return TraversalControl.CONTINUE; + } + } + + +} diff --git a/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy new file mode 100644 index 0000000000..b6666f8d67 --- /dev/null +++ b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy @@ -0,0 +1,112 @@ +package graphql.util + + +import graphql.TestUtil +import spock.lang.Specification + +class CyclicSchemaAnalyzerTest extends Specification { + + def "simple cycle"() { + given: + def sdl = ''' + + type Query { + hello: [Foo] + } + type Foo { + foo: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 1 + cycles[0].toString() == "Foo/0 -> foo/0" + + } + + def "multiple cycles"() { + given: + def sdl = ''' + + type Query { + hello: [Foo] + } + type Foo { + bar: Bar + foo: Foo + } + type Bar { + bar: [Bar]! + foo: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 3 + cycles[0].toString() == "Bar/0 -> bar/0 -> [Bar]!/0 -> [Bar]/0" + cycles[1].toString() == "Foo/0 -> bar/0 -> Bar/1 -> foo/0" + cycles[2].toString() == "Foo/1 -> foo/0" + + } + + def "larger cycle"() { + given: + def sdl = ''' + + type Query { + hello: [Foo] + } + type Foo { + bar: Bar + } + type Bar { + subBar: SubBar + } + type SubBar { + foo: Foo + } + + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 1 + cycles[0].toString() == "Foo/0 -> bar/0 -> Bar/0 -> subBar/0 -> SubBar/0 -> foo/0" + + } + + def "two parents and no cycle"() { + given: + def sdl = ''' + + type Query { + hello: Foo1 + hello2: Foo2 + } + type Foo1 { + bar: Bar + } + type Foo2 { + bar: Bar + } + type Bar { + id: ID + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 0 + + } +} From 9b881c042393115544c0805d30478fd9de2119ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:24:52 +0000 Subject: [PATCH 337/393] Bump com.fasterxml.jackson.core:jackson-databind from 2.16.2 to 2.17.0 Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.16.2 to 2.17.0. - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 08662b8725..15d512e833 100644 --- a/build.gradle +++ b/build.gradle @@ -111,7 +111,7 @@ dependencies { testImplementation 'org.codehaus.groovy:groovy-json:3.0.21' testImplementation 'com.google.code.gson:gson:2.10.1' testImplementation 'org.eclipse.jetty:jetty-server:11.0.20' - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.2' + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' testImplementation 'org.awaitility:awaitility-groovy:4.2.0' testImplementation 'com.github.javafaker:javafaker:1.0.2' From c2a4d877605b49a21ebfce713330d963d2336c88 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 19 Mar 2024 09:28:58 +1000 Subject: [PATCH 338/393] use correct algo --- .../graphql/introspection/Introspection.java | 4 + .../graphql/schema/diffing/SchemaGraph.java | 6 + .../graphql/util/CyclicSchemaAnalyzer.java | 363 +++++++++++++++--- .../util/CyclicSchemaAnalyzerTest.groovy | 10 +- 4 files changed, 329 insertions(+), 54 deletions(-) diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 4015ff393b..21e2291c34 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -772,6 +772,10 @@ public static boolean isIntrospectionTypes(GraphQLNamedType type) { return introspectionTypes.contains(type.getName()); } + public static boolean isIntrospectionTypes(String typeName) { + return introspectionTypes.contains(typeName); + } + /** * This will look up a field definition by name, and understand that fields like __typename and __schema are special * and take precedence in field resolution diff --git a/src/main/java/graphql/schema/diffing/SchemaGraph.java b/src/main/java/graphql/schema/diffing/SchemaGraph.java index edaebe5284..1bdcb43751 100644 --- a/src/main/java/graphql/schema/diffing/SchemaGraph.java +++ b/src/main/java/graphql/schema/diffing/SchemaGraph.java @@ -133,6 +133,9 @@ public List getAdjacentVerticesInverse(Vertex to, Predicate pred return result; } + public List getAdjacentEdges(Vertex from) { + return getAdjacentEdges(from, x -> true); + } public List getAdjacentEdges(Vertex from, Predicate predicate) { List result = new ArrayList<>(); for (Edge edge : edgesByDirection.row(from).values()) { @@ -295,4 +298,7 @@ public List getAllAdjacentEdges(List fromList, Vertex to) { return result; } + public boolean containsEdge(Vertex from, Vertex to) { + return this.edges.stream().anyMatch(edge -> edge.getFrom().equals(from) && edge.getTo().equals(to)); + } } diff --git a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java index 6b9858059d..1c4b779032 100644 --- a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java +++ b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java @@ -2,16 +2,20 @@ import graphql.ExperimentalApi; import graphql.introspection.Introspection; -import graphql.schema.GraphQLNamedType; import graphql.schema.GraphQLSchema; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLTypeUtil; -import graphql.schema.GraphQLTypeVisitorStub; -import graphql.schema.SchemaTraverser; +import graphql.schema.diffing.Edge; +import graphql.schema.diffing.SchemaGraph; +import graphql.schema.diffing.SchemaGraphFactory; +import graphql.schema.diffing.Vertex; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.StringJoiner; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * Finds all cycles in a GraphQL Schema. @@ -20,70 +24,331 @@ @ExperimentalApi public class CyclicSchemaAnalyzer { - public static class SchemaElementWithChild { - private final GraphQLSchemaElement element; - private final int childIndex; - - public SchemaElementWithChild(GraphQLSchemaElement element, int childIndex) { - this.element = element; - this.childIndex = childIndex; - } - } - public static class SchemaCycle { - private final List cycleElements = new ArrayList<>(); + private final List cyle; - public String toString() { - StringJoiner result = new StringJoiner(" -> "); - for (SchemaElementWithChild schemaElementWithChild : cycleElements) { - result.add(GraphQLTypeUtil.simplePrint(schemaElementWithChild.element) + "/" + schemaElementWithChild.childIndex); - } - return result.toString(); + public SchemaCycle(List cyle) { + this.cyle = cyle; } public int size() { - return cycleElements.size(); + return cyle.size(); + } + + @Override + public String toString() { + return cyle.toString(); } } public static List findCycles(GraphQLSchema schema) { - SchemaTraverser traverser = new SchemaTraverser(); - Visitor visitor = new Visitor(); - traverser.depthFirstFullSchema(visitor, schema); + FindCyclesImpl findCyclesImpl = new FindCyclesImpl(schema); + findCyclesImpl.findAllSimpleCyclesImpl(); + List> vertexCycles = findCyclesImpl.foundCycles; + vertexCycles = vertexCycles.stream().filter(vertices -> { + for (Vertex vertex : vertices) { + if (Introspection.isIntrospectionTypes(vertex.getName())) { + return false; + } + } + return true; + }).collect(Collectors.toList()); List result = new ArrayList<>(); - for (List> possibleCycle : visitor.cycles) { - SchemaCycle schemaCycle = new SchemaCycle(); - // the breadcrumbs are in the reverse order of the dependency - for (int i = possibleCycle.size() - 1; i >= 0; i--) { - Breadcrumb breadcrumb = possibleCycle.get(i); - SchemaElementWithChild schemaElementWithChild = new SchemaElementWithChild(breadcrumb.getNode(), breadcrumb.getLocation().getIndex()); - schemaCycle.cycleElements.add(schemaElementWithChild); + for (List vertexCycle : vertexCycles) { + List outputCycle = new ArrayList<>(); + for (Vertex vertex : vertexCycle) { + if (vertex.isOfType(SchemaGraph.OBJECT) || vertex.isOfType(SchemaGraph.INTERFACE)) { + outputCycle.add(vertex.getName()); + } else if (vertex.isOfType(SchemaGraph.FIELD)) { + String fieldsContainerName = findCyclesImpl.graph.getFieldsContainerForField(vertex).getName(); + outputCycle.add(fieldsContainerName + "." + vertex.getName()); + } } - result.add(schemaCycle); + result.add(new SchemaCycle(outputCycle)); } return result; } - private static class Visitor extends GraphQLTypeVisitorStub { - public final List>> cycles = new ArrayList<>(); + private static class GraphAndIndex { + final SchemaGraph graph; + final int index; - @Override - public TraversalControl visitBackRef(TraverserContext context) { - List> breadcrumbs = context.getBreadcrumbs(); + public GraphAndIndex(SchemaGraph graph, int index) { + this.graph = graph; + this.index = index; + } + } + + /** + * This code was originally taken from https://github.com/jgrapht/jgrapht/blob/master/jgrapht-core/src/main/java/org/jgrapht/alg/cycle/JohnsonSimpleCycles.java + * * (C) Copyright 2013-2023, by Nikolay Ognyanov and Contributors. + * * + * * JGraphT : a free Java graph-theory library + * * + * * See the CONTRIBUTORS.md file distributed with this work for additional + * * information regarding copyright ownership. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0, or the + * * GNU Lesser General Public License v2.1 or later + * * which is available at + * * http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html. + * * + * * SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later + */ + private static class FindCyclesImpl { + + private final GraphQLSchema schema; + private final SchemaGraph graph; + + // The main state of the algorithm. + private Vertex[] iToV = null; + private Map vToI = null; + private Set blocked = null; + private Map> bSets = null; + private ArrayDeque stack = null; + + // The state of the embedded Tarjan SCC algorithm. + private List> foundSCCs = null; + private int index = 0; + private Map vIndex = null; + private Map vLowlink = null; + private ArrayDeque path = null; + private Set pathSet = null; + + private List> foundCycles = new ArrayList<>(); + + public FindCyclesImpl(GraphQLSchema schema) { + this.schema = schema; + SchemaGraphFactory schemaGraphFactory = new SchemaGraphFactory(); + this.graph = schemaGraphFactory.createGraph(schema); + iToV = (Vertex[]) graph.getVertices().toArray(new Vertex[0]); + vToI = new HashMap<>(); + blocked = new HashSet<>(); + bSets = new HashMap<>(); + stack = new ArrayDeque<>(); + + for (int i = 0; i < iToV.length; i++) { + vToI.put(iToV[i], i); + } + } + + public List> findAllSimpleCyclesImpl() { + int startIndex = 0; + + int size = graph.getVertices().size(); + while (startIndex < size) { + GraphAndIndex minSCCGResult = findMinSCSG(startIndex); + if (minSCCGResult != null) { + startIndex = minSCCGResult.index; + SchemaGraph scg = minSCCGResult.graph; + Vertex startV = toV(startIndex); + for (Edge e : scg.getAdjacentEdges(startV)) { + Vertex v = e.getTo(); + blocked.remove(v); + getBSet(v).clear(); + } + findCyclesInSCG(startIndex, startIndex, scg); + startIndex++; + } else { + break; + } + } + return this.foundCycles; + } + + private GraphAndIndex findMinSCSG(int startIndex) { + /* + * Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph + * of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected + * component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum + * (among those SCCs) vertex index. We return that index together with the graph. + */ + initMinSCGState(); + + List> foundSCCs = findSCCS(startIndex); + + // find the SCC with the minimum index + int minIndexFound = Integer.MAX_VALUE; + Set minSCC = null; + for (Set scc : foundSCCs) { + for (Vertex v : scc) { + int t = toI(v); + if (t < minIndexFound) { + minIndexFound = t; + minSCC = scc; + } + } + } + if (minSCC == null) { + return null; + } + + // build a graph for the SCC found + SchemaGraph resultGraph = new SchemaGraph(); + for (Vertex v : minSCC) { + resultGraph.addVertex(v); + } + for (Vertex v : minSCC) { + for (Vertex w : minSCC) { + Edge edge = graph.getEdge(v, w); + if (edge != null) { + resultGraph.addEdge(edge); + } + } + } + + GraphAndIndex graphAndIndex = new GraphAndIndex(resultGraph, minIndexFound); + clearMinSCCState(); + return graphAndIndex; + } - for (int i = breadcrumbs.size() - 1; i >= 0; i--) { - Breadcrumb breadcrumb = breadcrumbs.get(i); - if (breadcrumb.getNode() instanceof GraphQLNamedType && Introspection.isIntrospectionTypes((GraphQLNamedType) breadcrumb.getNode())) { - return TraversalControl.CONTINUE; + private List> findSCCS(int startIndex) { + // Find SCCs in the subgraph induced + // by vertices startIndex and beyond. + // A call to StrongConnectivityAlgorithm + // would be too expensive because of the + // need to materialize the subgraph. + // So - do a local search by the Tarjan's + // algorithm and pretend that vertices + // with an index smaller than startIndex + // do not exist. + for (Vertex v : graph.getVertices()) { + int vI = toI(v); + if (vI < startIndex) { + continue; } - if (breadcrumb.getNode() == context.thisNode()) { - List> cycle = breadcrumbs.subList(0, i + 1); - cycles.add(cycle); + if (!vIndex.containsKey(v)) { + getSCCs(startIndex, vI); } } - return TraversalControl.CONTINUE; + List> result = foundSCCs; + foundSCCs = null; + return result; } - } + private void getSCCs(int startIndex, int vertexIndex) { + Vertex vertex = toV(vertexIndex); + vIndex.put(vertex, index); + vLowlink.put(vertex, index); + index++; + path.push(vertex); + pathSet.add(vertex); + List edges = graph.getAdjacentEdges(vertex); + for (Edge e : edges) { + Vertex successor = e.getTo(); + int successorIndex = toI(successor); + if (successorIndex < startIndex) { + continue; + } + if (!vIndex.containsKey(successor)) { + getSCCs(startIndex, successorIndex); + vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor))); + } else if (pathSet.contains(successor)) { + vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor))); + } + } + if (vLowlink.get(vertex).equals(vIndex.get(vertex))) { + Set result = new HashSet<>(); + Vertex temp; + do { + temp = path.pop(); + pathSet.remove(temp); + result.add(temp); + } while (!vertex.equals(temp)); + if (result.size() == 1) { + Vertex v = result.iterator().next(); + if (graph.containsEdge(vertex, v)) { + foundSCCs.add(result); + } + } else { + foundSCCs.add(result); + } + } + } + + private boolean findCyclesInSCG(int startIndex, int vertexIndex, SchemaGraph scg) { + /* + * Find cycles in a strongly connected graph per Johnson. + */ + boolean foundCycle = false; + Vertex vertex = toV(vertexIndex); + stack.push(vertex); + blocked.add(vertex); + + for (Edge e : scg.getAdjacentEdges(vertex)) { + Vertex successor = e.getTo(); + int successorIndex = toI(successor); + if (successorIndex == startIndex) { + List cycle = new ArrayList<>(stack.size()); + stack.descendingIterator().forEachRemaining(cycle::add); + this.foundCycles.add(cycle); + foundCycle = true; + } else if (!blocked.contains(successor)) { + boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg); + foundCycle = foundCycle || gotCycle; + } + } + if (foundCycle) { + unblock(vertex); + } else { + for (Edge ew : scg.getAdjacentEdges(vertex)) { + Vertex w = ew.getTo(); + Set bSet = getBSet(w); + bSet.add(vertex); + } + } + stack.pop(); + return foundCycle; + } + + private void unblock(Vertex vertex) { + blocked.remove(vertex); + Set bSet = getBSet(vertex); + while (bSet.size() > 0) { + Vertex w = bSet.iterator().next(); + bSet.remove(w); + if (blocked.contains(w)) { + unblock(w); + } + } + } + + + private void initMinSCGState() { + index = 0; + foundSCCs = new ArrayList<>(); + vIndex = new HashMap<>(); + vLowlink = new HashMap<>(); + path = new ArrayDeque<>(); + pathSet = new HashSet<>(); + } + + private void clearMinSCCState() { + index = 0; + foundSCCs = null; + vIndex = null; + vLowlink = null; + path = null; + pathSet = null; + } + + private Integer toI(Vertex vertex) { + return vToI.get(vertex); + } + + private Vertex toV(Integer i) { + return iToV[i]; + } + + private Set getBSet(Vertex v) { + // B sets typically not all needed, + // so instantiate lazily. + return bSets.computeIfAbsent(v, k -> new HashSet<>()); + } + + + } } diff --git a/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy index b6666f8d67..db05b41834 100644 --- a/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy +++ b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy @@ -23,7 +23,7 @@ class CyclicSchemaAnalyzerTest extends Specification { then: cycles.size() == 1 - cycles[0].toString() == "Foo/0 -> foo/0" + cycles[0].toString() == "[Foo.foo, Foo]" } @@ -49,9 +49,9 @@ class CyclicSchemaAnalyzerTest extends Specification { then: cycles.size() == 3 - cycles[0].toString() == "Bar/0 -> bar/0 -> [Bar]!/0 -> [Bar]/0" - cycles[1].toString() == "Foo/0 -> bar/0 -> Bar/1 -> foo/0" - cycles[2].toString() == "Foo/1 -> foo/0" + cycles[0].toString() == "[Foo.bar, Bar, Bar.foo, Foo]" + cycles[1].toString() == "[Foo.foo, Foo]" + cycles[2].toString() == "[Bar.bar, Bar]" } @@ -79,7 +79,7 @@ class CyclicSchemaAnalyzerTest extends Specification { then: cycles.size() == 1 - cycles[0].toString() == "Foo/0 -> bar/0 -> Bar/0 -> subBar/0 -> SubBar/0 -> foo/0" + cycles[0].toString() == "[Foo.bar, Bar, Bar.subBar, SubBar, SubBar.foo, Foo]" } From 0c89d5632b552128f61c938420be0bb18e6e0a57 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 19 Mar 2024 10:09:41 +1000 Subject: [PATCH 339/393] testing and input cycles --- .../graphql/util/CyclicSchemaAnalyzer.java | 36 +++-- .../util/CyclicSchemaAnalyzerTest.groovy | 152 ++++++++++++++++++ 2 files changed, 174 insertions(+), 14 deletions(-) diff --git a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java index 1c4b779032..e9c896bd59 100644 --- a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java +++ b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java @@ -1,5 +1,6 @@ package graphql.util; +import graphql.Assert; import graphql.ExperimentalApi; import graphql.introspection.Introspection; import graphql.schema.GraphQLSchema; @@ -10,8 +11,8 @@ import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -55,16 +56,23 @@ public static List findCycles(GraphQLSchema schema) { }).collect(Collectors.toList()); List result = new ArrayList<>(); for (List vertexCycle : vertexCycles) { - List outputCycle = new ArrayList<>(); + List stringCycle = new ArrayList<>(); for (Vertex vertex : vertexCycle) { if (vertex.isOfType(SchemaGraph.OBJECT) || vertex.isOfType(SchemaGraph.INTERFACE)) { - outputCycle.add(vertex.getName()); + stringCycle.add(vertex.getName()); } else if (vertex.isOfType(SchemaGraph.FIELD)) { String fieldsContainerName = findCyclesImpl.graph.getFieldsContainerForField(vertex).getName(); - outputCycle.add(fieldsContainerName + "." + vertex.getName()); + stringCycle.add(fieldsContainerName + "." + vertex.getName()); + } else if (vertex.isOfType(SchemaGraph.INPUT_OBJECT)) { + stringCycle.add(vertex.getName()); + } else if (vertex.isOfType(SchemaGraph.INPUT_FIELD)) { + String inputFieldsContainerName = findCyclesImpl.graph.getFieldsContainerForField(vertex).getName(); + stringCycle.add(inputFieldsContainerName + "." + vertex.getName()); + } else { + Assert.assertShouldNeverHappen("unexpected vertex in cycle found: " + vertex); } } - result.add(new SchemaCycle(outputCycle)); + result.add(new SchemaCycle(stringCycle)); } return result; } @@ -124,9 +132,9 @@ public FindCyclesImpl(GraphQLSchema schema) { SchemaGraphFactory schemaGraphFactory = new SchemaGraphFactory(); this.graph = schemaGraphFactory.createGraph(schema); iToV = (Vertex[]) graph.getVertices().toArray(new Vertex[0]); - vToI = new HashMap<>(); - blocked = new HashSet<>(); - bSets = new HashMap<>(); + vToI = new LinkedHashMap<>(); + blocked = new LinkedHashSet<>(); + bSets = new LinkedHashMap<>(); stack = new ArrayDeque<>(); for (int i = 0; i < iToV.length; i++) { @@ -251,7 +259,7 @@ private void getSCCs(int startIndex, int vertexIndex) { } } if (vLowlink.get(vertex).equals(vIndex.get(vertex))) { - Set result = new HashSet<>(); + Set result = new LinkedHashSet<>(); Vertex temp; do { temp = path.pop(); @@ -320,10 +328,10 @@ private void unblock(Vertex vertex) { private void initMinSCGState() { index = 0; foundSCCs = new ArrayList<>(); - vIndex = new HashMap<>(); - vLowlink = new HashMap<>(); + vIndex = new LinkedHashMap<>(); + vLowlink = new LinkedHashMap<>(); path = new ArrayDeque<>(); - pathSet = new HashSet<>(); + pathSet = new LinkedHashSet<>(); } private void clearMinSCCState() { @@ -346,7 +354,7 @@ private Vertex toV(Integer i) { private Set getBSet(Vertex v) { // B sets typically not all needed, // so instantiate lazily. - return bSets.computeIfAbsent(v, k -> new HashSet<>()); + return bSets.computeIfAbsent(v, k -> new LinkedHashSet<>()); } diff --git a/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy index db05b41834..07e55803f5 100644 --- a/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy +++ b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy @@ -27,6 +27,50 @@ class CyclicSchemaAnalyzerTest extends Specification { } + def "simple cycle with interfaces"() { + given: + def sdl = ''' + + type Query { + hello: [Foo] + } + interface Foo { + foo: Foo + } + type Impl implements Foo { + foo: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 1 + cycles[0].toString() == "[Foo.foo, Foo]" + + } + + def "input field cycle"() { + given: + def sdl = ''' + type Query { + hello(i: I): String + } + input I { + foo: I + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 1 + cycles[0].toString() == "[I.foo, I]" + + } + def "multiple cycles"() { given: def sdl = ''' @@ -109,4 +153,112 @@ class CyclicSchemaAnalyzerTest extends Specification { cycles.size() == 0 } + + def "cycle test"() { + given: + def sdl = ''' + type Query { + foo: Foo + } + type Foo { + f1: Foo + f2: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 2 + cycles[0].toString() == "[Foo.f1, Foo]" + cycles[1].toString() == "[Foo.f2, Foo]" + + + } + + def "cycle test 2"() { + given: + def sdl = ''' + type Query { + foo: Foo + } + type Foo { + f1: Foo + f2: Bar + } + type Bar { + foo: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 2 + cycles[0].toString() == "[Foo.f1, Foo]" + cycles[1].toString() == "[Foo.f2, Bar, Bar.foo, Foo]" + + } + + def "cycle test 3"() { + given: + def sdl = ''' + type Query { + foo: Foo + } + type Foo { + issues: [IssueConnection] + } + type IssueConnection { + edges: [Edge] + nodes: [Issue] + } + type Edge { + node: Issue + } + type Issue { + foo: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + //TODO: should be 2 + cycles.size() == 2 + cycles[0].toString() == "[Foo.issues, IssueConnection, IssueConnection.nodes, Issue, Issue.foo, Foo]" + cycles[1].toString() == "[Foo.issues, IssueConnection, IssueConnection.edges, Edge, Edge.node, Issue, Issue.foo, Foo]" + + } + + def "cycle test 4"() { + given: + def sdl = ''' + type Query { + foo: Foo + } + type Foo { + issues: [IssueConnection] + } + type IssueConnection { + edges: [Edge] + nodes: [Foo] + } + type Edge { + node: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 2 + cycles[0].toString() == "[Foo.issues, IssueConnection, IssueConnection.nodes, Foo]" + cycles[1].toString() == "[Foo.issues, IssueConnection, IssueConnection.edges, Edge, Edge.node, Foo]" + + } } From 5d0f9d52b29b85837162038acfbefe2a7c5dcf87 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 19 Mar 2024 10:10:14 +1000 Subject: [PATCH 340/393] cleanup --- src/main/java/graphql/util/CyclicSchemaAnalyzer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java index e9c896bd59..3227c74921 100644 --- a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java +++ b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java @@ -26,19 +26,19 @@ public class CyclicSchemaAnalyzer { public static class SchemaCycle { - private final List cyle; + private final List cycle; - public SchemaCycle(List cyle) { - this.cyle = cyle; + public SchemaCycle(List cycle) { + this.cycle = cycle; } public int size() { - return cyle.size(); + return cycle.size(); } @Override public String toString() { - return cyle.toString(); + return cycle.toString(); } } From 55b029a5005b7cec46ffa8cb15e06218ce51c619 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 19 Mar 2024 17:23:52 +1000 Subject: [PATCH 341/393] improve analyzer --- .../graphql/util/CyclicSchemaAnalyzer.java | 26 ++++++---- .../util/CyclicSchemaAnalyzerTest.groovy | 47 +++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java index 3227c74921..069dba0783 100644 --- a/src/main/java/graphql/util/CyclicSchemaAnalyzer.java +++ b/src/main/java/graphql/util/CyclicSchemaAnalyzer.java @@ -36,6 +36,10 @@ public int size() { return cycle.size(); } + public List getCycle() { + return cycle; + } + @Override public String toString() { return cycle.toString(); @@ -43,22 +47,28 @@ public String toString() { } public static List findCycles(GraphQLSchema schema) { + return findCycles(schema, true); + } + + public static List findCycles(GraphQLSchema schema, boolean filterOutIntrospectionCycles) { FindCyclesImpl findCyclesImpl = new FindCyclesImpl(schema); findCyclesImpl.findAllSimpleCyclesImpl(); List> vertexCycles = findCyclesImpl.foundCycles; - vertexCycles = vertexCycles.stream().filter(vertices -> { - for (Vertex vertex : vertices) { - if (Introspection.isIntrospectionTypes(vertex.getName())) { - return false; + if (filterOutIntrospectionCycles) { + vertexCycles = vertexCycles.stream().filter(vertices -> { + for (Vertex vertex : vertices) { + if (Introspection.isIntrospectionTypes(vertex.getName())) { + return false; + } } - } - return true; - }).collect(Collectors.toList()); + return true; + }).collect(Collectors.toList()); + } List result = new ArrayList<>(); for (List vertexCycle : vertexCycles) { List stringCycle = new ArrayList<>(); for (Vertex vertex : vertexCycle) { - if (vertex.isOfType(SchemaGraph.OBJECT) || vertex.isOfType(SchemaGraph.INTERFACE)) { + if (vertex.isOfType(SchemaGraph.OBJECT) || vertex.isOfType(SchemaGraph.INTERFACE) || vertex.isOfType(SchemaGraph.UNION)) { stringCycle.add(vertex.getName()); } else if (vertex.isOfType(SchemaGraph.FIELD)) { String fieldsContainerName = findCyclesImpl.graph.getFieldsContainerForField(vertex).getName(); diff --git a/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy index 07e55803f5..d24c2289c6 100644 --- a/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy +++ b/src/test/groovy/graphql/util/CyclicSchemaAnalyzerTest.groovy @@ -261,4 +261,51 @@ class CyclicSchemaAnalyzerTest extends Specification { cycles[1].toString() == "[Foo.issues, IssueConnection, IssueConnection.edges, Edge, Edge.node, Foo]" } + + def "cycle with Union"() { + given: + def sdl = ''' + type Query { + foo: Foo + } + union Foo = Bar | Baz + type Bar { + bar: Foo + } + type Baz { + bar: Foo + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema) + + then: + cycles.size() == 2 + cycles[0].toString() == "[Foo, Baz, Baz.bar]" + cycles[1].toString() == "[Foo, Bar, Bar.bar]" + + } + + def "introspection cycles "() { + given: + def sdl = ''' + type Query { + hello: String + } + ''' + def schema = TestUtil.schema(sdl) + when: + def cycles = CyclicSchemaAnalyzer.findCycles(schema, false) + + then: + cycles.size() == 6 + cycles[0].toString() == "[__Type.fields, __Field, __Field.type, __Type]" + cycles[1].toString() == "[__Type.fields, __Field, __Field.args, __InputValue, __InputValue.type, __Type]" + cycles[2].toString() == "[__Type.interfaces, __Type]" + cycles[3].toString() == "[__Type.possibleTypes, __Type]" + cycles[4].toString() == "[__Type.inputFields, __InputValue, __InputValue.type, __Type]" + cycles[5].toString() == "[__Type.ofType, __Type]" + + } } From b94f152a53ee4c69acc8d4c08611272342bf6a7b Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 21 Mar 2024 11:33:51 +1000 Subject: [PATCH 342/393] add the ability to restrict the number of ENFs created --- .../ExecutableNormalizedOperationFactory.java | 38 +++- ...tableNormalizedOperationFactoryTest.groovy | 195 +++++++++++++++++- 2 files changed, 225 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 96d5193919..4639ab4bed 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -80,17 +80,20 @@ public static class Options { private final GraphQLContext graphQLContext; private final Locale locale; private final int maxChildrenDepth; + private final int maxFieldsCount; private final boolean deferSupport; private Options(GraphQLContext graphQLContext, Locale locale, int maxChildrenDepth, + int maxFieldsCount, boolean deferSupport) { this.graphQLContext = graphQLContext; this.locale = locale; this.maxChildrenDepth = maxChildrenDepth; this.deferSupport = deferSupport; + this.maxFieldsCount = maxFieldsCount; } public static Options defaultOptions() { @@ -98,6 +101,7 @@ public static Options defaultOptions() { GraphQLContext.getDefault(), Locale.getDefault(), Integer.MAX_VALUE, + Integer.MAX_VALUE, false); } @@ -111,7 +115,7 @@ public static Options defaultOptions() { * @return new options object to use */ public Options locale(Locale locale) { - return new Options(this.graphQLContext, locale, this.maxChildrenDepth, this.deferSupport); + return new Options(this.graphQLContext, locale, this.maxChildrenDepth, this.maxFieldsCount, this.deferSupport); } /** @@ -124,7 +128,7 @@ public Options locale(Locale locale) { * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { - return new Options(graphQLContext, this.locale, this.maxChildrenDepth, this.deferSupport); + return new Options(graphQLContext, this.locale, this.maxChildrenDepth, this.maxFieldsCount, this.deferSupport); } /** @@ -136,7 +140,19 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { - return new Options(this.graphQLContext, this.locale, maxChildrenDepth, this.deferSupport); + return new Options(this.graphQLContext, this.locale, maxChildrenDepth, this.maxFieldsCount, this.deferSupport); + } + + /** + * Controls the maximum number of ENFs created. Can be used to prevent + * against malicious operations. + * + * @param maxFieldsCount the max number of ENFs created + * + * @return new options object to use + */ + public Options maxFieldsCount(int maxFieldsCount) { + return new Options(this.graphQLContext, this.locale, maxChildrenDepth, maxFieldsCount, this.deferSupport); } /** @@ -148,7 +164,7 @@ public Options maxChildrenDepth(int maxChildrenDepth) { */ @ExperimentalApi public Options deferSupport(boolean deferSupport) { - return new Options(this.graphQLContext, this.locale, this.maxChildrenDepth, deferSupport); + return new Options(this.graphQLContext, this.locale, this.maxChildrenDepth, this.maxFieldsCount, deferSupport); } /** @@ -178,6 +194,10 @@ public int getMaxChildrenDepth() { return maxChildrenDepth; } + public int getMaxFieldsCount() { + return maxFieldsCount; + } + /** * @return whether support for defer is enabled * @@ -386,6 +406,7 @@ private static class ExecutableNormalizedOperationFactoryImpl { private final ImmutableMap.Builder normalizedFieldToMergedField = ImmutableMap.builder(); private final ImmutableMap.Builder normalizedFieldToQueryDirectives = ImmutableMap.builder(); private final ImmutableListMultimap.Builder coordinatesToNormalizedFields = ImmutableListMultimap.builder(); + private int fieldCount = 0; private ExecutableNormalizedOperationFactoryImpl( GraphQLSchema graphQLSchema, @@ -590,7 +611,10 @@ private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGro normalizedArgumentValues = ValuesResolver.getNormalizedArgumentValues(fieldDefinition.getArguments(), field.getArguments(), this.normalizedVariableValues); } ImmutableList objectTypeNames = map(objectTypes, GraphQLObjectType::getName); - + this.fieldCount++; + if (this.fieldCount > this.options.getMaxFieldsCount()) { + throw new AbortExecutionException("Maximum ENF count exceeded " + this.fieldCount + " > " + this.options.getMaxFieldsCount()); + } return ExecutableNormalizedField.newNormalizedField() .alias(field.getAlias()) .resolvedArguments(argumentValues) @@ -763,8 +787,8 @@ private void collectInlineFragment(List result, private NormalizedDeferredExecution buildDeferredExecution( List directives, - Set newPossibleObjects) { - if(!options.deferSupport) { + Set newPossibleObjects) { + if (!options.deferSupport) { return null; } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 6063e7e448..5825c53e8d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -3,10 +3,12 @@ package graphql.normalized import graphql.ExecutionInput import graphql.GraphQL import graphql.TestUtil +import graphql.execution.AbortExecutionException import graphql.execution.CoercedVariables import graphql.execution.MergedField import graphql.execution.RawVariables import graphql.execution.directives.QueryAppliedDirective +import graphql.introspection.IntrospectionQuery import graphql.language.Document import graphql.language.Field import graphql.language.FragmentDefinition @@ -1285,7 +1287,6 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - when: def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def coordinatesToNormalizedFields = tree.coordinatesToNormalizedFields @@ -2876,6 +2877,198 @@ fragment personName on Person { noExceptionThrown() } + def "big query exceeding fields count"() { + String schema = """ + type Query { + animal: Animal + } + interface Animal { + name: String + friends: [Friend] + } + union Pet = Dog | Cat + type Friend { + name: String + isBirdOwner: Boolean + isCatOwner: Boolean + pets: [Pet] + } + type Bird implements Animal { + name: String + friends: [Friend] + } + type Cat implements Animal { + name: String + friends: [Friend] + breed: String + } + type Dog implements Animal { + name: String + breed: String + friends: [Friend] + } + """ + + def garbageFields = IntStream.range(0, 1000) + .mapToObj { + """test_$it: friends { name }""" + } + .collect(Collectors.joining("\n")) + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = """ + { + animal { + name + otherName: name + ... on Animal { + name + } + ... on Cat { + name + friends { + ... on Friend { + isCatOwner + pets { + ... on Dog { + name + } + } + } + } + } + ... on Bird { + friends { + isBirdOwner + } + friends { + name + pets { + ... on Cat { + breed + } + } + } + } + ... on Dog { + name + } + $garbageFields + } + } + """ + + assertValidQuery(graphQLSchema, query) + + Document document = TestUtil.parseQuery(query) + + when: + def result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.emptyVariables(), + ExecutableNormalizedOperationFactory.Options.defaultOptions().maxFieldsCount(2013)) + + then: + def e = thrown(AbortExecutionException) + e.message == "Maximum ENF count exceeded 2014 > 2013" + } + + def "small query exceeding fields count"() { + String schema = """ + type Query { + hello: String + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = """ {hello a1: hello}""" + + assertValidQuery(graphQLSchema, query) + + Document document = TestUtil.parseQuery(query) + + when: + def result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.emptyVariables(), + ExecutableNormalizedOperationFactory.Options.defaultOptions().maxFieldsCount(1)) + + then: + def e = thrown(AbortExecutionException) + e.message == "Maximum ENF count exceeded 2 > 1" + + + } + + def "query not exceeding fields count"() { + String schema = """ + type Query { + dogs: [Dog] + } + type Dog { + name: String + breed: String + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = """ {dogs{name breed }}""" + + assertValidQuery(graphQLSchema, query) + + Document document = TestUtil.parseQuery(query) + + when: + def result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.emptyVariables(), + ExecutableNormalizedOperationFactory.Options.defaultOptions().maxFieldsCount(3)) + + then: + notThrown(AbortExecutionException) + + + } + + def "query with meta fields exceeding fields count"() { + String schema = """ + type Query { + hello: String + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = IntrospectionQuery.INTROSPECTION_QUERY + + assertValidQuery(graphQLSchema, query) + + Document document = TestUtil.parseQuery(query) + + when: + def result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.emptyVariables(), + ExecutableNormalizedOperationFactory.Options.defaultOptions().maxFieldsCount(188)) + println result.normalizedFieldToMergedField.size() + + then: + def e = thrown(AbortExecutionException) + e.message == "Maximum ENF count exceeded 189 > 188" + } + + private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperation( GraphQLSchema graphQLSchema, Document document, From 9d37ffc49f5393bcc3ac7fe4c7b3539c460f38fc Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 21 Mar 2024 16:59:03 +1000 Subject: [PATCH 343/393] handle aliases correctly for good faith check --- .../graphql/introspection/Introspection.java | 24 +++++++++---------- ...ithIntrospectionInstrumentationTest.groovy | 13 ++++++++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index c228f1c39f..b934f46071 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -115,20 +115,20 @@ public static boolean isEnabledJvmWide() { */ public static Optional isIntrospectionSensible(MergedSelectionSet mergedSelectionSet, ExecutionContext executionContext) { GraphQLContext graphQLContext = executionContext.getGraphQLContext(); - MergedField schemaField = mergedSelectionSet.getSubField(SchemaMetaFieldDef.getName()); - if (schemaField != null) { - if (!isIntrospectionEnabled(graphQLContext)) { - return mkDisabledError(schemaField); - } - } - MergedField typeField = mergedSelectionSet.getSubField(TypeMetaFieldDef.getName()); - if (typeField != null) { - if (!isIntrospectionEnabled(graphQLContext)) { - return mkDisabledError(typeField); + + boolean isIntrospection = false; + for (String key : mergedSelectionSet.getKeys()) { + String fieldName = mergedSelectionSet.getSubField(key).getName(); + if (fieldName.equals(SchemaMetaFieldDef.getName()) + || fieldName.equals(TypeMetaFieldDef.getName())) { + if (!isIntrospectionEnabled(graphQLContext)) { + return mkDisabledError(mergedSelectionSet.getSubField(key)); + } + isIntrospection = true; + break; } } - if (schemaField != null || typeField != null) - { + if (isIntrospection) { return GoodFaithIntrospection.checkIntrospection(executionContext); } return Optional.empty(); diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy index f1ffc2c570..81ac91b075 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -69,12 +69,25 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { alias1 : __type(name : "t1") { name } } """ | _ + // a case for __type with aliases + """ query badActor { + a1: __type(name : "t") { name } + a2 : __type(name : "t1") { name } + } + """ | _ // a case for schema repeated - dont ask twice """ query badActor { __schema { types { name} } alias1 : __schema { types { name} } } """ | _ + // a case for used aliases + """ query badActor { + a1: __schema { types { name} } + a2 : __schema { types { name} } + } + """ | _ + } def "mixed general queries and introspections will be stopped anyway"() { From 83a21e982b98b048a47946ad3756af7b7e362197 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 21 Mar 2024 22:50:33 +1100 Subject: [PATCH 344/393] Added good faith code to create ENO that is a fixed size --- .../graphql/execution/ExecutionContext.java | 2 +- .../introspection/GoodFaithIntrospection.java | 35 ++++++++++++++- .../ExecutableNormalizedOperation.java | 22 +++++++++- .../ExecutableNormalizedOperationFactory.java | 44 ++++++++++++++++--- ...ithIntrospectionInstrumentationTest.groovy | 15 +++++++ ...tableNormalizedOperationFactoryTest.groovy | 43 ++++++++++++++++-- 6 files changed, 147 insertions(+), 14 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 9cd45a4e30..3cc63fb835 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -86,7 +86,7 @@ public class ExecutionContext { this.errors.set(builder.errors); this.localContext = builder.localContext; this.executionInput = builder.executionInput; - queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables)); + this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables)); } diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospection.java b/src/main/java/graphql/introspection/GoodFaithIntrospection.java index 7e853016ac..0a95713efb 100644 --- a/src/main/java/graphql/introspection/GoodFaithIntrospection.java +++ b/src/main/java/graphql/introspection/GoodFaithIntrospection.java @@ -18,6 +18,8 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import static graphql.normalized.ExecutableNormalizedOperationFactory.Options; +import static graphql.normalized.ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation; import static graphql.schema.FieldCoordinates.coordinates; /** @@ -44,6 +46,14 @@ public class GoodFaithIntrospection { public static final String GOOD_FAITH_INTROSPECTION_DISABLED = "GOOD_FAITH_INTROSPECTION_DISABLED"; private static final AtomicBoolean ENABLED_STATE = new AtomicBoolean(true); + /** + * This is the maximum number of executable fields that can be in a good faith introspection query + */ + public static final int GOOD_FAITH_MAX_FIELDS_COUNT = 100; + /** + * This is the maximum depth a good faith introspection query can be + */ + public static final int GOOD_FAITH_MAX_DEPTH_COUNT = 20; /** * @return true if good faith introspection is enabled @@ -75,7 +85,7 @@ public static boolean enabledJvmWide(boolean flag) { public static Optional checkIntrospection(ExecutionContext executionContext) { if (isIntrospectionEnabled(executionContext.getGraphQLContext())) { - ExecutableNormalizedOperation operation = executionContext.getNormalizedQueryTree().get(); + ExecutableNormalizedOperation operation = mkOperation(executionContext); ImmutableListMultimap coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); for (Map.Entry entry : ALLOWED_FIELD_INSTANCES.entrySet()) { FieldCoordinates coordinates = entry.getKey(); @@ -90,6 +100,29 @@ public static Optional checkIntrospection(ExecutionContext exec return Optional.empty(); } + /** + * This makes an executable operation limited in size then which suits a good faith introspection query. This helps guard + * against malicious queries. + * + * @param executionContext the execution context + * + * @return an executable operation + */ + private static ExecutableNormalizedOperation mkOperation(ExecutionContext executionContext) { + Options options = Options.defaultOptions() + .maxFieldsCount(GOOD_FAITH_MAX_FIELDS_COUNT) + .maxChildrenDepth(GOOD_FAITH_MAX_DEPTH_COUNT) + .locale(executionContext.getLocale()) + .graphQLContext(executionContext.getGraphQLContext()); + + return createExecutableNormalizedOperation(executionContext.getGraphQLSchema(), + executionContext.getOperationDefinition(), + executionContext.getFragmentsByName(), + executionContext.getCoercedVariables(), + options); + + } + private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { if (!isEnabledJvmWide()) { return false; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index ce50c9931b..115ded79b3 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -31,6 +31,8 @@ public class ExecutableNormalizedOperation { private final Map normalizedFieldToMergedField; private final Map normalizedFieldToQueryDirectives; private final ImmutableListMultimap coordinatesToNormalizedFields; + private final int operationFieldCount; + private final int operationDepth; public ExecutableNormalizedOperation( OperationDefinition.Operation operation, @@ -39,8 +41,8 @@ public ExecutableNormalizedOperation( ImmutableListMultimap fieldToNormalizedField, Map normalizedFieldToMergedField, Map normalizedFieldToQueryDirectives, - ImmutableListMultimap coordinatesToNormalizedFields - ) { + ImmutableListMultimap coordinatesToNormalizedFields, + int operationDepth) { this.operation = operation; this.operationName = operationName; this.topLevelFields = topLevelFields; @@ -48,6 +50,8 @@ public ExecutableNormalizedOperation( this.normalizedFieldToMergedField = normalizedFieldToMergedField; this.normalizedFieldToQueryDirectives = normalizedFieldToQueryDirectives; this.coordinatesToNormalizedFields = coordinatesToNormalizedFields; + this.operationFieldCount = fieldToNormalizedField.keySet().size(); + this.operationDepth = operationDepth; } /** @@ -64,6 +68,20 @@ public String getOperationName() { return operationName; } + /** + * @return This returns how many {@link ExecutableNormalizedField}s are in the operation. + */ + public int getOperationFieldCount() { + return operationFieldCount; + } + + /** + * @return This returns the depth of the operation + */ + public int getOperationDepth() { + return operationDepth; + } + /** * This multimap shows how a given {@link ExecutableNormalizedField} maps to a one or more field coordinate in the schema * diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 4639ab4bed..ac73721b2f 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -64,6 +64,7 @@ import static graphql.util.FpKit.filterSet; import static graphql.util.FpKit.groupingBy; import static graphql.util.FpKit.intersection; +import static java.util.Collections.max; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toCollection; @@ -286,13 +287,36 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( OperationDefinition operationDefinition, Map fragments, CoercedVariables coercedVariableValues) { + return createExecutableNormalizedOperation(graphQLSchema, + operationDefinition, + fragments, + coercedVariableValues, + Options.defaultOptions()); + } + + /** + * This will create a runtime representation of the graphql operation that would be executed + * in a runtime sense. + * + * @param graphQLSchema the schema to be used + * @param operationDefinition the operation to be executed + * @param fragments a set of fragments associated with the operation + * @param coercedVariableValues the coerced variables to use + * + * @return a runtime representation of the graphql operation. + */ + public static ExecutableNormalizedOperation createExecutableNormalizedOperation(GraphQLSchema graphQLSchema, + OperationDefinition operationDefinition, + Map fragments, + CoercedVariables coercedVariableValues, + Options options) { return new ExecutableNormalizedOperationFactoryImpl( graphQLSchema, operationDefinition, fragments, coercedVariableValues, null, - Options.defaultOptions() + options ).createNormalizedQueryImpl(); } @@ -432,6 +456,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { CollectNFResult collectFromOperationResult = collectFromOperation(rootType); + int maxDepthSeen = 0; for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); MergedField mergedField = newMergedField(fieldAndAstParents); @@ -441,10 +466,11 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { updateFieldToNFMap(topLevel, fieldAndAstParents); updateCoordinatedToNFMap(topLevel); - buildFieldWithChildren( + int depthSeen = buildFieldWithChildren( topLevel, fieldAndAstParents, 1); + maxDepthSeen = Math.max(maxDepthSeen,depthSeen); } // getPossibleMergerList for (PossibleMerger possibleMerger : possibleMergerList) { @@ -458,7 +484,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { fieldToNormalizedField.build(), normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), - coordinatesToNormalizedFields.build() + coordinatesToNormalizedFields.build(), + maxDepthSeen ); } @@ -469,15 +496,16 @@ private void captureMergedField(ExecutableNormalizedField enf, MergedField merge normalizedFieldToMergedField.put(enf, mergedFld); } - private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, + private int buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, ImmutableList fieldAndAstParents, int curLevel) { if (curLevel > this.options.getMaxChildrenDepth()) { - throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + this.options.getMaxChildrenDepth()); + throw new AbortExecutionException("Maximum query depth exceeded. " + curLevel + " > " + this.options.getMaxChildrenDepth()); } CollectNFResult nextLevel = collectFromMergedField(executableNormalizedField, fieldAndAstParents, curLevel + 1); + int maxDepthSeen = curLevel; for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); @@ -488,10 +516,12 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz updateFieldToNFMap(childENF, childFieldAndAstParents); updateCoordinatedToNFMap(childENF); - buildFieldWithChildren(childENF, + int depthSeen = buildFieldWithChildren(childENF, childFieldAndAstParents, curLevel + 1); + maxDepthSeen = Math.max(maxDepthSeen,depthSeen); } + return maxDepthSeen; } private static MergedField newMergedField(ImmutableList fieldAndAstParents) { @@ -613,7 +643,7 @@ private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGro ImmutableList objectTypeNames = map(objectTypes, GraphQLObjectType::getName); this.fieldCount++; if (this.fieldCount > this.options.getMaxFieldsCount()) { - throw new AbortExecutionException("Maximum ENF count exceeded " + this.fieldCount + " > " + this.options.getMaxFieldsCount()); + throw new AbortExecutionException("Maximum field count exceeded. " + this.fieldCount + " > " + this.options.getMaxFieldsCount()); } return ExecutableNormalizedField.newNormalizedField() .alias(field.getAlias()) diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy index 81ac91b075..1af10dd098 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -3,6 +3,9 @@ package graphql.introspection import graphql.ExecutionInput import graphql.ExecutionResult import graphql.TestUtil +import graphql.execution.CoercedVariables +import graphql.language.Document +import graphql.normalized.ExecutableNormalizedOperationFactory import spock.lang.Specification class GoodFaithIntrospectionInstrumentationTest extends Specification { @@ -16,6 +19,18 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { GoodFaithIntrospection.enabledJvmWide(true) } + def "standard introspection query is inside limits just in general"() { + + when: + Document document = TestUtil.toDocument(IntrospectionQuery.INTROSPECTION_QUERY) + def eno = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphql.getGraphQLSchema(),document, + "IntrospectionQuery", CoercedVariables.emptyVariables()) + + then: + eno.getOperationFieldCount() < GoodFaithIntrospection.GOOD_FAITH_MAX_FIELDS_COUNT // currently 62 + eno.getOperationDepth() < GoodFaithIntrospection.GOOD_FAITH_MAX_DEPTH_COUNT // currently 13 + } + def "test asking for introspection in good faith"() { when: diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 5825c53e8d..29f49e55dc 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -2973,7 +2973,7 @@ fragment personName on Person { then: def e = thrown(AbortExecutionException) - e.message == "Maximum ENF count exceeded 2014 > 2013" + e.message == "Maximum field count exceeded. 2014 > 2013" } def "small query exceeding fields count"() { @@ -3001,7 +3001,7 @@ fragment personName on Person { then: def e = thrown(AbortExecutionException) - e.message == "Maximum ENF count exceeded 2 > 1" + e.message == "Maximum field count exceeded. 2 > 1" } @@ -3065,9 +3065,46 @@ fragment personName on Person { then: def e = thrown(AbortExecutionException) - e.message == "Maximum ENF count exceeded 189 > 188" + e.message == "Maximum field count exceeded. 189 > 188" } + def "can capture depth and field count"() { + String schema = """ + type Query { + foo: Foo + } + + type Foo { + stop : String + bar : Bar + } + + type Bar { + stop : String + foo : Foo + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = "{ foo { bar { foo { bar { foo { stop bar { stop }}}}}}}" + + assertValidQuery(graphQLSchema, query) + + Document document = TestUtil.parseQuery(query) + + when: + def result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.emptyVariables() + ) + + then: + result.getOperationDepth() == 7 + result.getOperationFieldCount() == 8 + } private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperation( GraphQLSchema graphQLSchema, From 2c29b536f5dd1bbd1d14ed1f69309e1f7354cfb5 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Mar 2024 10:28:43 +1100 Subject: [PATCH 345/393] Uppped limit as discussed --- src/main/java/graphql/introspection/GoodFaithIntrospection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospection.java b/src/main/java/graphql/introspection/GoodFaithIntrospection.java index 0a95713efb..37a1406500 100644 --- a/src/main/java/graphql/introspection/GoodFaithIntrospection.java +++ b/src/main/java/graphql/introspection/GoodFaithIntrospection.java @@ -49,7 +49,7 @@ public class GoodFaithIntrospection { /** * This is the maximum number of executable fields that can be in a good faith introspection query */ - public static final int GOOD_FAITH_MAX_FIELDS_COUNT = 100; + public static final int GOOD_FAITH_MAX_FIELDS_COUNT = 500; /** * This is the maximum depth a good faith introspection query can be */ From bd6ec10c13146cc3a001db89e6842bd973064949 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Mar 2024 12:31:31 +1100 Subject: [PATCH 346/393] Fixed tests and also added unit test for depth in good faith --- .../ExecutableNormalizedOperation.java | 3 +- .../ExecutableNormalizedOperationFactory.java | 12 +-- ...nterfacesImplementingInterfacesTest.groovy | 44 +++++++++-- src/test/groovy/graphql/UnionTest.groovy | 39 +++++++--- ...ithIntrospectionInstrumentationTest.groovy | 78 +++++++++++++++++-- 5 files changed, 145 insertions(+), 31 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index 115ded79b3..cfcda2746d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -42,6 +42,7 @@ public ExecutableNormalizedOperation( Map normalizedFieldToMergedField, Map normalizedFieldToQueryDirectives, ImmutableListMultimap coordinatesToNormalizedFields, + int operationFieldCount, int operationDepth) { this.operation = operation; this.operationName = operationName; @@ -50,7 +51,7 @@ public ExecutableNormalizedOperation( this.normalizedFieldToMergedField = normalizedFieldToMergedField; this.normalizedFieldToQueryDirectives = normalizedFieldToQueryDirectives; this.coordinatesToNormalizedFields = coordinatesToNormalizedFields; - this.operationFieldCount = fieldToNormalizedField.keySet().size(); + this.operationFieldCount = operationFieldCount; this.operationDepth = operationDepth; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index ac73721b2f..35b1defe2d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -431,6 +431,7 @@ private static class ExecutableNormalizedOperationFactoryImpl { private final ImmutableMap.Builder normalizedFieldToQueryDirectives = ImmutableMap.builder(); private final ImmutableListMultimap.Builder coordinatesToNormalizedFields = ImmutableListMultimap.builder(); private int fieldCount = 0; + private int maxDepthSeen = 0; private ExecutableNormalizedOperationFactoryImpl( GraphQLSchema graphQLSchema, @@ -456,7 +457,6 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { CollectNFResult collectFromOperationResult = collectFromOperation(rootType); - int maxDepthSeen = 0; for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); MergedField mergedField = newMergedField(fieldAndAstParents); @@ -485,6 +485,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), coordinatesToNormalizedFields.build(), + fieldCount, maxDepthSeen ); } @@ -629,6 +630,11 @@ private void createNFs(ImmutableList.Builder nfListBu private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGroup, int level, ExecutableNormalizedField parent) { + + this.fieldCount++; + if (this.fieldCount > this.options.getMaxFieldsCount()) { + throw new AbortExecutionException("Maximum field count exceeded. " + this.fieldCount + " > " + this.options.getMaxFieldsCount()); + } Field field; Set objectTypes = collectedFieldGroup.objectTypes; field = collectedFieldGroup.fields.iterator().next().field; @@ -641,10 +647,6 @@ private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGro normalizedArgumentValues = ValuesResolver.getNormalizedArgumentValues(fieldDefinition.getArguments(), field.getArguments(), this.normalizedVariableValues); } ImmutableList objectTypeNames = map(objectTypes, GraphQLObjectType::getName); - this.fieldCount++; - if (this.fieldCount > this.options.getMaxFieldsCount()) { - throw new AbortExecutionException("Maximum field count exceeded. " + this.fieldCount + " > " + this.options.getMaxFieldsCount()); - } return ExecutableNormalizedField.newNormalizedField() .alias(field.getAlias()) .resolvedArguments(argumentValues) diff --git a/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy b/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy index b5813c4d0b..bb22d70461 100644 --- a/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy +++ b/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy @@ -893,8 +893,10 @@ class InterfacesImplementingInterfacesTest extends Specification { given: def graphQLSchema = createComplexSchema() + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() + when: - def result = GraphQL.newGraphQL(graphQLSchema).build().execute(""" + String query = """ { nodeType: __type(name: "Node") { possibleTypes { @@ -902,7 +904,20 @@ class InterfacesImplementingInterfacesTest extends Specification { name } } - resourceType: __type(name: "Resource") { + } + """ + def result = graphQL.execute(query) + + then: + !result.errors + result.data == [ + nodeType: [possibleTypes: [[kind: 'OBJECT', name: 'File'], [kind: 'OBJECT', name: 'Image']]], + ] + + when: + query = """ + { + resourceType: __type(name: "Resource") { possibleTypes { kind name @@ -911,22 +926,35 @@ class InterfacesImplementingInterfacesTest extends Specification { kind name } - } - imageType: __type(name: "Image") { + } + } + """ + result = graphQL.execute(query) + + then: + !result.errors + result.data == [ + resourceType: [possibleTypes: [[kind: 'OBJECT', name: 'File'], [kind: 'OBJECT', name: 'Image']], interfaces: [[kind: 'INTERFACE', name: 'Node']]] + ] + + when: + + query = """ + { + imageType: __type(name: "Image") { interfaces { kind name } } - } - """) + } + """ + result = graphQL.execute(query) then: !result.errors result.data == [ - nodeType : [possibleTypes: [[kind: 'OBJECT', name: 'File'], [kind: 'OBJECT', name: 'Image']]], imageType : [interfaces: [[kind: 'INTERFACE', name: 'Resource'], [kind: 'INTERFACE', name: 'Node']]], - resourceType: [possibleTypes: [[kind: 'OBJECT', name: 'File'], [kind: 'OBJECT', name: 'Image']], interfaces: [[kind: 'INTERFACE', name: 'Node']]] ] } diff --git a/src/test/groovy/graphql/UnionTest.groovy b/src/test/groovy/graphql/UnionTest.groovy index 403f31d3d8..8edd7b2600 100644 --- a/src/test/groovy/graphql/UnionTest.groovy +++ b/src/test/groovy/graphql/UnionTest.groovy @@ -4,7 +4,7 @@ import spock.lang.Specification class UnionTest extends Specification { - def "can introspect on union and intersection types"() { + def "can introspect on union types"() { def query = """ { Named: __type(name: "Named") { @@ -15,15 +15,6 @@ class UnionTest extends Specification { possibleTypes { name } enumValues { name } inputFields { name } - } - Pet: __type(name: "Pet") { - kind - name - fields { name } - interfaces { name } - possibleTypes { name } - enumValues { name } - inputFields { name } } } """ @@ -42,8 +33,32 @@ class UnionTest extends Specification { ], enumValues : null, inputFields : null - ], - Pet : [ + ]] + when: + def executionResult = GraphQL.newGraphQL(GarfieldSchema.GarfieldSchema).build().execute(query) + + then: + executionResult.data == expectedResult + + + } + + def "can introspect on intersection types"() { + def query = """ + { + Pet: __type(name: "Pet") { + kind + name + fields { name } + interfaces { name } + possibleTypes { name } + enumValues { name } + inputFields { name } + } + } + """ + + def expectedResult = [Pet : [ kind : 'UNION', name : 'Pet', fields : null, diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy index 1af10dd098..b77e1d76e8 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy @@ -3,6 +3,7 @@ package graphql.introspection import graphql.ExecutionInput import graphql.ExecutionResult import graphql.TestUtil +import graphql.execution.AbortExecutionException import graphql.execution.CoercedVariables import graphql.language.Document import graphql.normalized.ExecutableNormalizedOperationFactory @@ -15,6 +16,7 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { def setup() { GoodFaithIntrospection.enabledJvmWide(true) } + def cleanup() { GoodFaithIntrospection.enabledJvmWide(true) } @@ -23,11 +25,11 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { when: Document document = TestUtil.toDocument(IntrospectionQuery.INTROSPECTION_QUERY) - def eno = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphql.getGraphQLSchema(),document, - "IntrospectionQuery", CoercedVariables.emptyVariables()) + def eno = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphql.getGraphQLSchema(), document, + "IntrospectionQuery", CoercedVariables.emptyVariables()) then: - eno.getOperationFieldCount() < GoodFaithIntrospection.GOOD_FAITH_MAX_FIELDS_COUNT // currently 62 + eno.getOperationFieldCount() < GoodFaithIntrospection.GOOD_FAITH_MAX_FIELDS_COUNT // currently 189 eno.getOperationDepth() < GoodFaithIntrospection.GOOD_FAITH_MAX_DEPTH_COUNT // currently 13 } @@ -89,7 +91,7 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { a1: __type(name : "t") { name } a2 : __type(name : "t1") { name } } - """ | _ + """ | _ // a case for schema repeated - dont ask twice """ query badActor { __schema { types { name} } @@ -101,7 +103,7 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { a1: __schema { types { name} } a2 : __schema { types { name} } } - """ | _ + """ | _ } @@ -161,4 +163,70 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { !er.errors.isEmpty() er.errors[0] instanceof GoodFaithIntrospection.BadFaithIntrospectionError } + + def "can stop deep queries"() { + + when: + def query = createDeepQuery(depth) + def then = System.currentTimeMillis() + ExecutionResult er = graphql.execute(query) + def ms = System.currentTimeMillis()-then + + then: + !er.errors.isEmpty() + er.errors[0].class == targetError + er.data == null // it stopped hard - it did not continue to normal business + println "Took " + ms + "ms" + + where: + depth | targetError + 2 | GoodFaithIntrospection.BadFaithIntrospectionError.class + 10 | AbortExecutionException.class + 15 | AbortExecutionException.class + 20 | AbortExecutionException.class + 25 | AbortExecutionException.class + 50 | AbortExecutionException.class + 100 | AbortExecutionException.class + } + + String createDeepQuery(int depth = 25) { + def result = """ +query test { + __schema { + types { + ...F1 + } + } +} +""" + for (int i = 1; i < depth; i++) { + result += """ + fragment F$i on __Type { + fields { + type { + ...F${i + 1} + } + } + + ofType { + ...F${i + 1} + } +} + + +""" + } + result += """ + fragment F$depth on __Type { + fields { + type { +name + } + } +} + + +""" + return result + } } From 55f2e5d4e275da2568169f53ed38a34657b0e33c Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Mar 2024 14:21:37 +1100 Subject: [PATCH 347/393] Added benchmark code --- .../ENFBenchmarkDeepIntrospection.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/test/java/benchmark/ENFBenchmarkDeepIntrospection.java diff --git a/src/test/java/benchmark/ENFBenchmarkDeepIntrospection.java b/src/test/java/benchmark/ENFBenchmarkDeepIntrospection.java new file mode 100644 index 0000000000..0ed09d4675 --- /dev/null +++ b/src/test/java/benchmark/ENFBenchmarkDeepIntrospection.java @@ -0,0 +1,122 @@ +package benchmark; + +import graphql.execution.CoercedVariables; +import graphql.language.Document; +import graphql.normalized.ExecutableNormalizedOperation; +import graphql.normalized.ExecutableNormalizedOperationFactory; +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaGenerator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +import static graphql.normalized.ExecutableNormalizedOperationFactory.*; + +@State(Scope.Benchmark) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3, time = 5) +@Fork(2) +public class ENFBenchmarkDeepIntrospection { + + @Param({"2", "10", "20"}) + int howDeep = 2; + + String query = ""; + + GraphQLSchema schema; + Document document; + + @Setup(Level.Trial) + public void setUp() { + String schemaString = BenchmarkUtils.loadResource("large-schema-2.graphqls"); + schema = SchemaGenerator.createdMockedSchema(schemaString); + + query = createDeepQuery(howDeep); + document = Parser.parse(query); + } + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public ExecutableNormalizedOperation benchMarkAvgTime() { + ExecutableNormalizedOperationFactory.Options options = ExecutableNormalizedOperationFactory.Options.defaultOptions(); + ExecutableNormalizedOperation executableNormalizedOperation = createExecutableNormalizedOperation(schema, + document, + null, + CoercedVariables.emptyVariables(), + options); + return executableNormalizedOperation; + } + + public static void main(String[] args) throws RunnerException { + runAtStartup(); + + Options opt = new OptionsBuilder() + .include("benchmark.ENFBenchmarkDeepIntrospection") + .build(); + + new Runner(opt).run(); + } + + private static void runAtStartup() { + + ENFBenchmarkDeepIntrospection benchmarkIntrospection = new ENFBenchmarkDeepIntrospection(); + benchmarkIntrospection.howDeep = 2; + + BenchmarkUtils.runInToolingForSomeTimeThenExit( + benchmarkIntrospection::setUp, + () -> { while (true) { benchmarkIntrospection.benchMarkAvgTime(); }}, + () ->{} + ); + } + + + + private static String createDeepQuery(int depth) { + String result = "query test {\n" + + " __schema {\n" + + " types {\n" + + " ...F1\n" + + " }\n" + + " }\n" + + "}\n"; + + for (int i = 1; i < depth; i++) { + result += " fragment F" + i + " on __Type {\n" + + " fields {\n" + + " type {\n" + + " ...F" + (i + 1) +"\n" + + " }\n" + + " }\n" + + "\n" + + " ofType {\n" + + " ...F"+ (i + 1) + "\n" + + " }\n" + + " }\n"; + } + result += " fragment F" + depth + " on __Type {\n" + + " fields {\n" + + " type {\n" + + "name\n" + + " }\n" + + " }\n" + + "}\n"; + return result; + } + +} From 70895d7b8e3c31a61feedd0e54ab0ddef80f1f0c Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Mar 2024 14:56:32 +1100 Subject: [PATCH 348/393] Check max depth seen more often --- .../ExecutableNormalizedOperationFactory.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 35b1defe2d..ffc89ca87e 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -500,9 +500,7 @@ private void captureMergedField(ExecutableNormalizedField enf, MergedField merge private int buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, ImmutableList fieldAndAstParents, int curLevel) { - if (curLevel > this.options.getMaxChildrenDepth()) { - throw new AbortExecutionException("Maximum query depth exceeded. " + curLevel + " > " + this.options.getMaxChildrenDepth()); - } + checkMaxDepthExceeded(curLevel); CollectNFResult nextLevel = collectFromMergedField(executableNormalizedField, fieldAndAstParents, curLevel + 1); @@ -521,10 +519,18 @@ private int buildFieldWithChildren(ExecutableNormalizedField executableNormalize childFieldAndAstParents, curLevel + 1); maxDepthSeen = Math.max(maxDepthSeen,depthSeen); + + checkMaxDepthExceeded(maxDepthSeen); } return maxDepthSeen; } + private void checkMaxDepthExceeded(int depthSeen) { + if (depthSeen > this.options.getMaxChildrenDepth()) { + throw new AbortExecutionException("Maximum query depth exceeded. " + depthSeen + " > " + this.options.getMaxChildrenDepth()); + } + } + private static MergedField newMergedField(ImmutableList fieldAndAstParents) { return MergedField.newMergedField(map(fieldAndAstParents, fieldAndAstParent -> fieldAndAstParent.field)).build(); } From d5eb1609d908c92e6cb698be1ce7f2d4d29bc87a Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 25 Mar 2024 16:12:12 +1000 Subject: [PATCH 349/393] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4d692c6973..e065da305d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Discuss and ask questions in our Discussions: https://github.com/graphql-java/gr This is a [GraphQL](https://github.com/graphql/graphql-spec) Java implementation. +Latest build in Maven central: https://repo1.maven.org/maven2/com/graphql-java/graphql-java/ + [![Build](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml/badge.svg)](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml) [![Latest Release](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?versionPrefix=21.)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) [![Latest Snapshot](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?label=maven-central%20snapshot&versionPrefix=0)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) From 20b1071236f439996aff10170946423ca2aa7f16 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 26 Mar 2024 14:07:35 +1000 Subject: [PATCH 350/393] cleanup --- .../normalized/ExecutableNormalizedOperationFactory.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index ffc89ca87e..735ab899d9 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -64,7 +64,6 @@ import static graphql.util.FpKit.filterSet; import static graphql.util.FpKit.groupingBy; import static graphql.util.FpKit.intersection; -import static java.util.Collections.max; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toCollection; @@ -153,7 +152,7 @@ public Options maxChildrenDepth(int maxChildrenDepth) { * @return new options object to use */ public Options maxFieldsCount(int maxFieldsCount) { - return new Options(this.graphQLContext, this.locale, maxChildrenDepth, maxFieldsCount, this.deferSupport); + return new Options(this.graphQLContext, this.locale, this.maxChildrenDepth, maxFieldsCount, this.deferSupport); } /** From 6b242d4ffb6bea34ba3cfbae3db89a35081bbe10 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 26 Mar 2024 16:09:16 +1100 Subject: [PATCH 351/393] Removed old usages and deprecated the old --- .../java/graphql/analysis/NodeVisitorWithTypeTracking.java | 4 ++-- src/main/java/graphql/introspection/Introspection.java | 2 ++ .../normalized/ExecutableNormalizedOperationFactory.java | 5 +++-- .../ExecutableNormalizedOperationToAstCompiler.java | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java index 683138e387..fc33070e8c 100644 --- a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java +++ b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java @@ -152,7 +152,7 @@ public TraversalControl visitField(Field field, TraverserContext context) QueryTraversalContext parentEnv = context.getVarFromParents(QueryTraversalContext.class); GraphQLContext graphQLContext = parentEnv.getGraphQLContext(); - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(schema, (GraphQLCompositeType) unwrapAll(parentEnv.getOutputType()), field.getName()); + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDefinition(schema, (GraphQLFieldsContainer) unwrapAll(parentEnv.getOutputType()), field.getName()); boolean isTypeNameIntrospectionField = fieldDefinition == schema.getIntrospectionTypenameFieldDefinition(); GraphQLFieldsContainer fieldsContainer = !isTypeNameIntrospectionField ? (GraphQLFieldsContainer) unwrapAll(parentEnv.getOutputType()) : null; GraphQLCodeRegistry codeRegistry = schema.getCodeRegistry(); @@ -203,7 +203,7 @@ public TraversalControl visitArgument(Argument argument, TraverserContext QueryVisitorFieldEnvironment fieldEnv = fieldCtx.getEnvironment(); GraphQLFieldsContainer fieldsContainer = fieldEnv.getFieldsContainer(); - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(schema, fieldsContainer, field.getName()); + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDefinition(schema, fieldsContainer, field.getName()); GraphQLArgument graphQLArgument = fieldDefinition.getArgument(argument.getName()); String argumentName = graphQLArgument.getName(); diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 79c2ef3a48..7bf0c70338 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -792,6 +792,7 @@ public static boolean isIntrospectionTypes(String typeName) { * * @return a field definition otherwise throws an assertion exception if it's null */ + @Deprecated(since = "2024-03-24", forRemoval = true) public static GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLCompositeType parentType, String fieldName) { GraphQLFieldDefinition fieldDefinition = getSystemFieldDef(schema, parentType, fieldName); @@ -800,6 +801,7 @@ public static GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLCo } assertTrue(parentType instanceof GraphQLFieldsContainer, () -> String.format("should not happen : parent type must be an object or interface %s", parentType)); + @SuppressWarnings("DataFlowIssue") GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) parentType; fieldDefinition = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, fieldName); assertTrue(fieldDefinition != null, () -> String.format("Unknown field '%s' for type %s", fieldName, fieldsContainer.getName())); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 735ab899d9..07b449e69a 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -34,6 +34,7 @@ import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; import graphql.schema.GraphQLObjectType; @@ -562,7 +563,7 @@ public CollectNFResult collectFromMergedField(ExecutableNormalizedField executab if (fieldAndAstParent.field.getSelectionSet() == null) { continue; } - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(graphQLSchema, fieldAndAstParent.astParentType, fieldAndAstParent.field.getName()); + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDefinition(graphQLSchema, (GraphQLFieldsContainer) fieldAndAstParent.astParentType, fieldAndAstParent.field.getName()); GraphQLUnmodifiedType astParentType = unwrapAll(fieldDefinition.getType()); this.collectFromSelectionSet(fieldAndAstParent.field.getSelectionSet(), collectedFields, @@ -644,7 +645,7 @@ private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGro Set objectTypes = collectedFieldGroup.objectTypes; field = collectedFieldGroup.fields.iterator().next().field; String fieldName = field.getName(); - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(graphQLSchema, objectTypes.iterator().next(), fieldName); + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDefinition(graphQLSchema, objectTypes.iterator().next(), fieldName); Map argumentValues = ValuesResolver.getArgumentValues(fieldDefinition.getArguments(), field.getArguments(), CoercedVariables.of(this.coercedVariableValues.toMap()), this.options.graphQLContext, this.options.locale); Map normalizedArgumentValues = null; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 877decb767..daa70ecc0d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -24,8 +24,8 @@ import graphql.language.TypeName; import graphql.language.Value; import graphql.normalized.incremental.NormalizedDeferredExecution; -import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLUnmodifiedType; @@ -465,7 +465,7 @@ private static Value argValue(ExecutableNormalizedField executableNormalizedF private static GraphQLFieldDefinition getFieldDefinition(GraphQLSchema schema, String parentType, ExecutableNormalizedField nf) { - return Introspection.getFieldDef(schema, (GraphQLCompositeType) schema.getType(parentType), nf.getName()); + return Introspection.getFieldDefinition(schema, (GraphQLFieldsContainer) schema.getType(parentType), nf.getName()); } From a27be65e53075c28375718b528aefb6632569a2a Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 26 Mar 2024 16:42:04 +1100 Subject: [PATCH 352/393] No longer deprecated --- .../graphql/analysis/NodeVisitorWithTypeTracking.java | 2 +- src/main/java/graphql/introspection/Introspection.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java index fc33070e8c..2a2b237aeb 100644 --- a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java +++ b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java @@ -152,7 +152,7 @@ public TraversalControl visitField(Field field, TraverserContext context) QueryTraversalContext parentEnv = context.getVarFromParents(QueryTraversalContext.class); GraphQLContext graphQLContext = parentEnv.getGraphQLContext(); - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDefinition(schema, (GraphQLFieldsContainer) unwrapAll(parentEnv.getOutputType()), field.getName()); + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(schema, (GraphQLCompositeType) unwrapAll(parentEnv.getOutputType()), field.getName()); boolean isTypeNameIntrospectionField = fieldDefinition == schema.getIntrospectionTypenameFieldDefinition(); GraphQLFieldsContainer fieldsContainer = !isTypeNameIntrospectionField ? (GraphQLFieldsContainer) unwrapAll(parentEnv.getOutputType()) : null; GraphQLCodeRegistry codeRegistry = schema.getCodeRegistry(); diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 7bf0c70338..ced6cbf818 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -784,7 +784,8 @@ public static boolean isIntrospectionTypes(String typeName) { /** * This will look up a field definition by name, and understand that fields like __typename and __schema are special - * and take precedence in field resolution + * and take precedence in field resolution. If the parent type is a union type, then the only field allowed + * is `__typename`. * * @param schema the schema to use * @param parentType the type of the parent object @@ -792,7 +793,6 @@ public static boolean isIntrospectionTypes(String typeName) { * * @return a field definition otherwise throws an assertion exception if it's null */ - @Deprecated(since = "2024-03-24", forRemoval = true) public static GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLCompositeType parentType, String fieldName) { GraphQLFieldDefinition fieldDefinition = getSystemFieldDef(schema, parentType, fieldName); @@ -800,11 +800,10 @@ public static GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLCo return fieldDefinition; } - assertTrue(parentType instanceof GraphQLFieldsContainer, () -> String.format("should not happen : parent type must be an object or interface %s", parentType)); - @SuppressWarnings("DataFlowIssue") + assertTrue(parentType instanceof GraphQLFieldsContainer, "should not happen : parent type must be an object or interface %s", parentType); GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) parentType; fieldDefinition = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, fieldName); - assertTrue(fieldDefinition != null, () -> String.format("Unknown field '%s' for type %s", fieldName, fieldsContainer.getName())); + assertTrue(fieldDefinition != null, "Unknown field '%s' for type %s", fieldName, fieldsContainer.getName()); return fieldDefinition; } From b5f3df8701f960f930a61a0cce72c0d1ac02c0e3 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 26 Mar 2024 16:44:04 +1100 Subject: [PATCH 353/393] Made old code the same --- .../normalized/ExecutableNormalizedOperationFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 07b449e69a..7ed78470d5 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -563,7 +563,7 @@ public CollectNFResult collectFromMergedField(ExecutableNormalizedField executab if (fieldAndAstParent.field.getSelectionSet() == null) { continue; } - GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDefinition(graphQLSchema, (GraphQLFieldsContainer) fieldAndAstParent.astParentType, fieldAndAstParent.field.getName()); + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(graphQLSchema, fieldAndAstParent.astParentType, fieldAndAstParent.field.getName()); GraphQLUnmodifiedType astParentType = unwrapAll(fieldDefinition.getType()); this.collectFromSelectionSet(fieldAndAstParent.field.getSelectionSet(), collectedFields, From d65674130203cb1f29ed9567edb1db80caf533d9 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 26 Mar 2024 16:48:50 +1100 Subject: [PATCH 354/393] Made old code the same - more of that --- .../normalized/ExecutableNormalizedOperationToAstCompiler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index daa70ecc0d..b8ccc21d43 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -24,6 +24,7 @@ import graphql.language.TypeName; import graphql.language.Value; import graphql.normalized.incremental.NormalizedDeferredExecution; +import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLObjectType; @@ -465,7 +466,7 @@ private static Value argValue(ExecutableNormalizedField executableNormalizedF private static GraphQLFieldDefinition getFieldDefinition(GraphQLSchema schema, String parentType, ExecutableNormalizedField nf) { - return Introspection.getFieldDefinition(schema, (GraphQLFieldsContainer) schema.getType(parentType), nf.getName()); + return Introspection.getFieldDef(schema, (GraphQLCompositeType) schema.getType(parentType), nf.getName()); } From 9669753c7563776375b38289f5bbe3b8fbe49655 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 26 Mar 2024 16:49:37 +1100 Subject: [PATCH 355/393] Made old code the same - more of that - bad import --- .../normalized/ExecutableNormalizedOperationToAstCompiler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index b8ccc21d43..877decb767 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -26,7 +26,6 @@ import graphql.normalized.incremental.NormalizedDeferredExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLUnmodifiedType; From 595f48878d846251e3fd2a19ad1ab3e0acad2844 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 26 Mar 2024 16:54:28 +1000 Subject: [PATCH 356/393] catch Abort execution exceptions and turn them into proper errors --- .../introspection/GoodFaithIntrospection.java | 32 ++++++++++++++++--- ...oovy => GoodFaithIntrospectionTest.groovy} | 17 +++++----- 2 files changed, 35 insertions(+), 14 deletions(-) rename src/test/groovy/graphql/introspection/{GoodFaithIntrospectionInstrumentationTest.groovy => GoodFaithIntrospectionTest.groovy} (93%) diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospection.java b/src/main/java/graphql/introspection/GoodFaithIntrospection.java index 37a1406500..bd7285cbd1 100644 --- a/src/main/java/graphql/introspection/GoodFaithIntrospection.java +++ b/src/main/java/graphql/introspection/GoodFaithIntrospection.java @@ -7,6 +7,7 @@ import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.PublicApi; +import graphql.execution.AbortExecutionException; import graphql.execution.ExecutionContext; import graphql.language.SourceLocation; import graphql.normalized.ExecutableNormalizedField; @@ -85,14 +86,20 @@ public static boolean enabledJvmWide(boolean flag) { public static Optional checkIntrospection(ExecutionContext executionContext) { if (isIntrospectionEnabled(executionContext.getGraphQLContext())) { - ExecutableNormalizedOperation operation = mkOperation(executionContext); + ExecutableNormalizedOperation operation; + try { + operation = mkOperation(executionContext); + } catch (AbortExecutionException e) { + BadFaithIntrospectionError error = BadFaithIntrospectionError.tooBigOperation(e.getMessage()); + return Optional.of(ExecutionResult.newExecutionResult().addError(error).build()); + } ImmutableListMultimap coordinatesToENFs = operation.getCoordinatesToNormalizedFields(); for (Map.Entry entry : ALLOWED_FIELD_INSTANCES.entrySet()) { FieldCoordinates coordinates = entry.getKey(); Integer allowSize = entry.getValue(); ImmutableList normalizedFields = coordinatesToENFs.get(coordinates); if (normalizedFields.size() > allowSize) { - BadFaithIntrospectionError error = new BadFaithIntrospectionError(coordinates.toString()); + BadFaithIntrospectionError error = BadFaithIntrospectionError.tooManyFields(coordinates.toString()); return Optional.of(ExecutionResult.newExecutionResult().addError(error).build()); } } @@ -108,7 +115,7 @@ public static Optional checkIntrospection(ExecutionContext exec * * @return an executable operation */ - private static ExecutableNormalizedOperation mkOperation(ExecutionContext executionContext) { + private static ExecutableNormalizedOperation mkOperation(ExecutionContext executionContext) throws AbortExecutionException { Options options = Options.defaultOptions() .maxFieldsCount(GOOD_FAITH_MAX_FIELDS_COUNT) .maxChildrenDepth(GOOD_FAITH_MAX_DEPTH_COUNT) @@ -133,8 +140,16 @@ private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { public static class BadFaithIntrospectionError implements GraphQLError { private final String message; - public BadFaithIntrospectionError(String qualifiedField) { - this.message = String.format("This request is not asking for introspection in good faith - %s is present too often!", qualifiedField); + public static BadFaithIntrospectionError tooManyFields(String fieldCoordinate) { + return new BadFaithIntrospectionError(String.format("This request is not asking for introspection in good faith - %s is present too often!", fieldCoordinate)); + } + + public static BadFaithIntrospectionError tooBigOperation(String message) { + return new BadFaithIntrospectionError(String.format("This request is not asking for introspection in good faith - the query is too big: %s", message)); + } + + private BadFaithIntrospectionError(String message) { + this.message = message; } @Override @@ -151,5 +166,12 @@ public ErrorClassification getErrorType() { public List getLocations() { return null; } + + @Override + public String toString() { + return "BadFaithIntrospectionError{" + + "message='" + message + '\'' + + '}'; + } } } diff --git a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionTest.groovy similarity index 93% rename from src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy rename to src/test/groovy/graphql/introspection/GoodFaithIntrospectionTest.groovy index b77e1d76e8..c2d9b2dc87 100644 --- a/src/test/groovy/graphql/introspection/GoodFaithIntrospectionInstrumentationTest.groovy +++ b/src/test/groovy/graphql/introspection/GoodFaithIntrospectionTest.groovy @@ -3,13 +3,12 @@ package graphql.introspection import graphql.ExecutionInput import graphql.ExecutionResult import graphql.TestUtil -import graphql.execution.AbortExecutionException import graphql.execution.CoercedVariables import graphql.language.Document import graphql.normalized.ExecutableNormalizedOperationFactory import spock.lang.Specification -class GoodFaithIntrospectionInstrumentationTest extends Specification { +class GoodFaithIntrospectionTest extends Specification { def graphql = TestUtil.graphQL("type Query { normalField : String }").build() @@ -170,7 +169,7 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { def query = createDeepQuery(depth) def then = System.currentTimeMillis() ExecutionResult er = graphql.execute(query) - def ms = System.currentTimeMillis()-then + def ms = System.currentTimeMillis() - then then: !er.errors.isEmpty() @@ -181,12 +180,12 @@ class GoodFaithIntrospectionInstrumentationTest extends Specification { where: depth | targetError 2 | GoodFaithIntrospection.BadFaithIntrospectionError.class - 10 | AbortExecutionException.class - 15 | AbortExecutionException.class - 20 | AbortExecutionException.class - 25 | AbortExecutionException.class - 50 | AbortExecutionException.class - 100 | AbortExecutionException.class + 10 | GoodFaithIntrospection.BadFaithIntrospectionError.class + 15 | GoodFaithIntrospection.BadFaithIntrospectionError.class + 20 | GoodFaithIntrospection.BadFaithIntrospectionError.class + 25 | GoodFaithIntrospection.BadFaithIntrospectionError.class + 50 | GoodFaithIntrospection.BadFaithIntrospectionError.class + 100 | GoodFaithIntrospection.BadFaithIntrospectionError.class } String createDeepQuery(int depth = 25) { From a999912d392bc33eb8218fa740c226cd282ecbd0 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 26 Mar 2024 17:12:15 +1000 Subject: [PATCH 357/393] add a default max nodes count --- .../ExecutableNormalizedOperationFactory.java | 11 ++--- ...tableNormalizedOperationFactoryTest.groovy | 48 ++++++++++++++++++- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index ffc89ca87e..647f9f66c4 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -64,7 +64,6 @@ import static graphql.util.FpKit.filterSet; import static graphql.util.FpKit.groupingBy; import static graphql.util.FpKit.intersection; -import static java.util.Collections.max; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toCollection; @@ -102,7 +101,7 @@ public static Options defaultOptions() { GraphQLContext.getDefault(), Locale.getDefault(), Integer.MAX_VALUE, - Integer.MAX_VALUE, + 100_000, false); } @@ -470,7 +469,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { topLevel, fieldAndAstParents, 1); - maxDepthSeen = Math.max(maxDepthSeen,depthSeen); + maxDepthSeen = Math.max(maxDepthSeen, depthSeen); } // getPossibleMergerList for (PossibleMerger possibleMerger : possibleMergerList) { @@ -498,8 +497,8 @@ private void captureMergedField(ExecutableNormalizedField enf, MergedField merge } private int buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, - ImmutableList fieldAndAstParents, - int curLevel) { + ImmutableList fieldAndAstParents, + int curLevel) { checkMaxDepthExceeded(curLevel); CollectNFResult nextLevel = collectFromMergedField(executableNormalizedField, fieldAndAstParents, curLevel + 1); @@ -518,7 +517,7 @@ private int buildFieldWithChildren(ExecutableNormalizedField executableNormalize int depthSeen = buildFieldWithChildren(childENF, childFieldAndAstParents, curLevel + 1); - maxDepthSeen = Math.max(maxDepthSeen,depthSeen); + maxDepthSeen = Math.max(maxDepthSeen, depthSeen); checkMaxDepthExceeded(maxDepthSeen); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 29f49e55dc..0890866a00 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -3099,13 +3099,59 @@ fragment personName on Person { document, null, RawVariables.emptyVariables() - ) + ) then: result.getOperationDepth() == 7 result.getOperationFieldCount() == 8 } + def "factory has a default max node count"() { + String schema = """ + type Query { + foo: Foo + } + type Foo { + foo: Foo + name: String + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = "{ foo { ...F1}} " + int fragmentCount = 12 + for (int i = 1; i < fragmentCount; i++) { + query += """ + fragment F$i on Foo { + foo { ...F${i + 1} } + a: foo{ ...F${i + 1} } + b: foo{ ...F${i + 1} } + } + """ + } + query += """ + fragment F$fragmentCount on Foo{ + name + } + """ + + assertValidQuery(graphQLSchema, query) + + Document document = TestUtil.parseQuery(query) + + when: + def result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.emptyVariables() + ) + then: + def e = thrown(AbortExecutionException) + e.message == "Maximum field count exceeded. 100001 > 100000" + } + private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperation( GraphQLSchema graphQLSchema, Document document, From 378459f6b4c91636296e7c4477078a3f0c636cc4 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 26 Mar 2024 18:55:28 +1100 Subject: [PATCH 358/393] Merged in master and fixed up some code --- src/main/java/graphql/execution/ExecutionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 3d66d4587f..df32c930c0 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -754,7 +754,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { if (resultNodesCount > maxNodes) { executionContext.getResultNodesInfo().maxResultNodesExceeded(); - return new FieldValueInfo(NULL, completedFuture(null), fieldValueInfos); + return new FieldValueInfo(NULL, null, fieldValueInfos); } } From ec4b7ae4ee5eb0a17093a8c7ad058bd343f2f20c Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 27 Mar 2024 09:50:51 +1000 Subject: [PATCH 359/393] add possibility to override default options --- .../ExecutableNormalizedOperationFactory.java | 36 +++++++++++++++---- ...tableNormalizedOperationFactoryTest.groovy | 35 ++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 647f9f66c4..5191fbc8cf 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -77,6 +77,8 @@ public class ExecutableNormalizedOperationFactory { public static class Options { + + private final GraphQLContext graphQLContext; private final Locale locale; private final int maxChildrenDepth; @@ -84,6 +86,18 @@ public static class Options { private final boolean deferSupport; + /** + * The default max fields count is 100,000. + * This is big enough for even very large queries, but + * can be changed via {#setDefaultOptions + */ + public static final int DEFAULT_MAX_FIELDS_COUNT = 100_000; + private static Options defaultOptions = new Options(GraphQLContext.getDefault(), + Locale.getDefault(), + Integer.MAX_VALUE, + DEFAULT_MAX_FIELDS_COUNT, + false); + private Options(GraphQLContext graphQLContext, Locale locale, int maxChildrenDepth, @@ -96,13 +110,23 @@ private Options(GraphQLContext graphQLContext, this.maxFieldsCount = maxFieldsCount; } + /** + * Sets new default Options used when creating instances of {@link ExecutableNormalizedOperation}. + * + * @param options new default options + */ + public static void setDefaultOptions(Options options) { + defaultOptions = Assert.assertNotNull(options); + } + + + /** + * Returns the default options used when creating instances of {@link ExecutableNormalizedOperation}. + * + * @return the default options + */ public static Options defaultOptions() { - return new Options( - GraphQLContext.getDefault(), - Locale.getDefault(), - Integer.MAX_VALUE, - 100_000, - false); + return defaultOptions; } /** diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 0890866a00..9797a9b285 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -3152,6 +3152,41 @@ fragment personName on Person { e.message == "Maximum field count exceeded. 100001 > 100000" } + def "default max fields can be changed "() { + String schema = """ + type Query { + foo: Foo + } + type Foo { + foo: Foo + name: String + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = "{ foo { foo{ name}}} " + + assertValidQuery(graphQLSchema, query) + + Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory.Options.setDefaultOptions(ExecutableNormalizedOperationFactory.Options.defaultOptions().maxFieldsCount(2)) + + when: + def result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.emptyVariables() + ) + then: + def e = thrown(AbortExecutionException) + e.message == "Maximum field count exceeded. 3 > 2" + cleanup: + ExecutableNormalizedOperationFactory.Options.setDefaultOptions(ExecutableNormalizedOperationFactory.Options.defaultOptions().maxFieldsCount(ExecutableNormalizedOperationFactory.Options.DEFAULT_MAX_FIELDS_COUNT)) + } + + private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperation( GraphQLSchema graphQLSchema, Document document, From d1b901240508e26fcb0bf88fa46e66855564915b Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 27 Mar 2024 10:09:51 +1000 Subject: [PATCH 360/393] cleanup --- .../normalized/ExecutableNormalizedOperationFactoryTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 9797a9b285..2b9f146721 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -3165,7 +3165,7 @@ fragment personName on Person { GraphQLSchema graphQLSchema = TestUtil.schema(schema) - String query = "{ foo { foo{ name}}} " + String query = "{foo{foo{name}}} " assertValidQuery(graphQLSchema, query) From fbb690924d38abc223a3e25ba2c9fc2a78ab4953 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Wed, 27 Mar 2024 15:50:14 +1100 Subject: [PATCH 361/393] Fixed up java agent --- .../java/graphql/agent/GraphQLJavaAgent.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java index a172724671..1dd9f1fe42 100644 --- a/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java +++ b/agent/src/main/java/graphql/agent/GraphQLJavaAgent.java @@ -170,26 +170,33 @@ public static void invokeDataFetcherEnter(@Advice.Argument(0) ExecutionContext e @Advice.OnMethodExit public static void invokeDataFetcherExit(@Advice.Argument(0) ExecutionContext executionContext, @Advice.Argument(1) ExecutionStrategyParameters parameters, - @Advice.Return(readOnly = false) CompletableFuture result) { + @Advice.Return(readOnly = false) Object cfOrObject) { // ExecutionTrackingResult executionTrackingResult = executionContext.getGraphQLContext().get(EXECUTION_TRACKING_KEY); ExecutionTrackingResult executionTrackingResult = GraphQLJavaAgent.executionIdToData.get(executionContext.getExecutionId()); ResultPath path = parameters.getPath(); long startTime = executionTrackingResult.timePerPath.get(path); executionTrackingResult.end(path, System.nanoTime()); - if (result.isDone()) { - if (result.isCancelled()) { - executionTrackingResult.setDfResultTypes(path, DONE_CANCELLED); - } else if (result.isCompletedExceptionally()) { - executionTrackingResult.setDfResultTypes(path, DONE_EXCEPTIONALLY); + if (cfOrObject instanceof CompletableFuture) { + CompletableFuture result = (CompletableFuture) cfOrObject; + if (result.isDone()) { + if (result.isCancelled()) { + executionTrackingResult.setDfResultTypes(path, DONE_CANCELLED); + } else if (result.isCompletedExceptionally()) { + executionTrackingResult.setDfResultTypes(path, DONE_EXCEPTIONALLY); + } else { + executionTrackingResult.setDfResultTypes(path, DONE_OK); + } } else { - executionTrackingResult.setDfResultTypes(path, DONE_OK); + executionTrackingResult.setDfResultTypes(path, PENDING); } + // overriding the result to make sure the finished handler is called first when the DF is finished + // otherwise it is a completion tree instead of chain + cfOrObject = result.whenComplete(new DataFetcherFinishedHandler(executionContext, parameters, startTime)); } else { - executionTrackingResult.setDfResultTypes(path, PENDING); + // materialized value - not a CF + executionTrackingResult.setDfResultTypes(path, DONE_OK); + new DataFetcherFinishedHandler(executionContext, parameters, startTime).accept(cfOrObject, null); } - // overriding the result to make sure the finished handler is called first when the DF is finished - // otherwise it is a completion tree instead of chain - result = result.whenComplete(new DataFetcherFinishedHandler(executionContext, parameters, startTime)); } } From 38e702b73b9357f59efa3c4105f12715464230a6 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 28 Mar 2024 17:04:19 +1100 Subject: [PATCH 362/393] Extra tests for materialized versus promised objects --- .../MaterialisedAndPromisedObjectsTest.groovy | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy diff --git a/src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy b/src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy new file mode 100644 index 0000000000..de827e21e9 --- /dev/null +++ b/src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy @@ -0,0 +1,94 @@ +package graphql.execution + + +import graphql.ExecutionResult +import graphql.GraphQL +import graphql.TestUtil +import graphql.execution.instrumentation.Instrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimplePerformantInstrumentation +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import spock.lang.Specification + +import java.util.concurrent.CompletableFuture + +import static graphql.ExecutionInput.newExecutionInput + +class MaterialisedAndPromisedObjectsTest extends Specification { + + def sdl = """ + type Query { + foo : Foo + } + + type Foo { + bar : Bar + name : String + } + + type Bar { + foo : Foo + name : String + } + """ + + def "make sure it can fetch both materialised and promised values"() { + + def cfPromisesOnFieldRegex = ~"neverMatchesAlwaysMaterialised" + Instrumentation fetchSwitcher = new SimplePerformantInstrumentation() { + @Override + DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + return new DataFetcher() { + @Override + Object get(DataFetchingEnvironment env) throws Exception { + def fieldName = env.getField().name + def fetchValue = dataFetcher.get(env) + // if it matches the regex - we send back an async promise value + if (fieldName =~ cfPromisesOnFieldRegex) { + return CompletableFuture.supplyAsync { -> fetchValue } + } + // just the materialised value! + return fetchValue + } + } + } + } + + GraphQL graphQL = TestUtil.graphQL(sdl).instrumentation(fetchSwitcher).build() + + + def source = [foo: [bar: [foo: [name: "stop"]]]] + def expectedData = [foo: [bar: [foo: [name: "stop"]]]] + + def query = """ { foo { bar { foo { name }}}} """ + + + when: "always materialised - no promises" + ExecutionResult er = graphQL.execute(newExecutionInput(query).root(source)) + + + then: + er.errors.isEmpty() + er.data == expectedData + + when: "everything is promises" + + cfPromisesOnFieldRegex = ~".*" + er = graphQL.execute(newExecutionInput(query).root(source)) + + then: + er.errors.isEmpty() + er.data == expectedData + + + when: "only foo fields are CF promises so a mix of materialised and promised values" + cfPromisesOnFieldRegex = ~"foo" + er = graphQL.execute(newExecutionInput(query).root(source)) + + then: + er.errors.isEmpty() + er.data == expectedData + } +} From 3eee54f33c2056db875e0d7fc6794448e2cc54d4 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 28 Mar 2024 17:11:33 +1100 Subject: [PATCH 363/393] Extra tests for materialized versus promised objects - tweaked test code --- .../execution/MaterialisedAndPromisedObjectsTest.groovy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy b/src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy index de827e21e9..578e3b3ea9 100644 --- a/src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy +++ b/src/test/groovy/graphql/execution/MaterialisedAndPromisedObjectsTest.groovy @@ -66,6 +66,8 @@ class MaterialisedAndPromisedObjectsTest extends Specification { when: "always materialised - no promises" + + cfPromisesOnFieldRegex = ~"neverMatchesAlwaysMaterialised" ExecutionResult er = graphQL.execute(newExecutionInput(query).root(source)) @@ -84,6 +86,7 @@ class MaterialisedAndPromisedObjectsTest extends Specification { when: "only foo fields are CF promises so a mix of materialised and promised values" + cfPromisesOnFieldRegex = ~"foo" er = graphQL.execute(newExecutionInput(query).root(source)) From 09d8c69c0a17c12b8cedbffb8a97fe85cbf9f835 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 28 Mar 2024 18:52:54 +1100 Subject: [PATCH 364/393] Removed deprecated methods and fixed up javadoc --- .../graphql/execution/ExecutionStrategy.java | 39 +++++++----- .../graphql/execution/FieldValueInfo.java | 54 ++++++++++++++--- .../incremental/DeferredExecutionSupport.java | 6 +- .../execution/ExecutionStrategyTest.groovy | 60 +++++++++---------- 4 files changed, 107 insertions(+), 52 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 413c2c0619..e50ba6fcb9 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -191,9 +191,9 @@ public static String mkNameForPath(List currentField) { * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * - * @return a promise to a map of object field values + * @return a {@link CompletableFuture} promise to a map of object field values or a materialized map of object field values * - * @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value + * @throws NonNullableFieldWasNullException in the {@link CompletableFuture} if a non-null field resolved to a null value */ @SuppressWarnings("unchecked") protected Object /* CompletableFuture> | Map */ executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { @@ -256,7 +256,7 @@ public static String mkNameForPath(List currentField) { return overallResult; } else { Map fieldValueMap = buildFieldValueMap(fieldsExecutedOnInitialResult, (List) completedValuesObject); - resolveObjectCtx.onCompleted(fieldValueMap,null); + resolveObjectCtx.onCompleted(fieldValueMap, null); return fieldValueMap; } } @@ -349,9 +349,9 @@ Async.CombinedBuilder getAsyncFieldValueInfo( * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * - * @return a promise to an {@link Object} + * @return a {@link CompletableFuture} promise to an {@link Object} or the materialized {@link Object} * - * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value + * @throws NonNullableFieldWasNullException in the future if a non-null field resolved to a null value */ @SuppressWarnings("unchecked") protected Object /* CompletableFuture | Object */ resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -375,9 +375,10 @@ Async.CombinedBuilder getAsyncFieldValueInfo( * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * - * @return a promise to a {@link FieldValueInfo} + * @return a {@link CompletableFuture} promise to a {@link FieldValueInfo} or a materialised {@link FieldValueInfo} * - * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value + * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValueFuture()} future + * if a nonnull field resolves to a null value */ @SuppressWarnings("unchecked") protected Object /* CompletableFuture | FieldValueInfo */ resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { @@ -393,7 +394,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( if (fetchedValueObj instanceof CompletableFuture) { CompletableFuture fetchFieldFuture = (CompletableFuture) fetchedValueObj; CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> - completeField(fieldDef,executionContext, parameters, fetchedValue)); + completeField(fieldDef, executionContext, parameters, fetchedValue)); fieldCtx.onDispatched(); result.whenComplete(fieldCtx::onCompleted); @@ -401,7 +402,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( } else { try { FetchedValue fetchedValue = (FetchedValue) fetchedValueObj; - FieldValueInfo fieldValueInfo = completeField(fieldDef,executionContext, parameters, fetchedValue); + FieldValueInfo fieldValueInfo = completeField(fieldDef, executionContext, parameters, fetchedValue); fieldCtx.onDispatched(); fieldCtx.onCompleted(fetchedValue.getFetchedValue(), null); return fieldValueInfo; @@ -493,7 +494,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); - Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); + Object fetchedObject = invokeDataFetcher(parameters, fieldDef, dataFetchingEnvironment, dataFetcher); executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject); fetchCtx.onDispatched(); if (fetchedObject instanceof CompletableFuture) { @@ -517,7 +518,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( } } - private Object invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { + private Object invokeDataFetcher(ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { Object fetchedValue; try { Object fetchedValueRaw; @@ -619,7 +620,8 @@ private CompletableFuture asyncHandleException(DataFetcherExceptionHandle * * @return a {@link FieldValueInfo} * - * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value + * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValueFuture()} future + * if a nonnull field resolves to a null value */ protected FieldValueInfo completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) { Field field = parameters.getField().getSingleField(); @@ -719,13 +721,22 @@ private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStra * * @return a {@link FieldValueInfo} * - * @throws NonNullableFieldWasNullException if a non null field resolves to a null value + * @throws NonNullableFieldWasNullException inside a {@link CompletableFuture} if a non null field resolves to a null value */ private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters parameters) { Object fieldValue = completeValueForNull(parameters); return new FieldValueInfo(NULL, fieldValue); } + /** + * Called to complete a null value. + * + * @param parameters contains the parameters holding the fields to be executed and source object + * + * @return a null value or a {@link CompletableFuture} exceptionally completed + * + * @throws NonNullableFieldWasNullException inside the {@link CompletableFuture} if a non-null field resolves to a null value + */ protected Object /* CompletableFuture | Object */ completeValueForNull(ExecutionStrategyParameters parameters) { try { return parameters.getNonNullFieldValidator().validate(parameters.getPath(), null); @@ -918,7 +929,7 @@ protected void handleValueException(CompletableFuture overallResult, Thro * @param resolvedObjectType the resolved object type * @param result the result to be coerced * - * @return a promise to an {@link ExecutionResult} + * @return a {@link CompletableFuture} promise to a map of object field values or a materialized map of object field values */ protected Object /* CompletableFuture> | Map */ completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); diff --git a/src/main/java/graphql/execution/FieldValueInfo.java b/src/main/java/graphql/execution/FieldValueInfo.java index d72657b354..283cad42c6 100644 --- a/src/main/java/graphql/execution/FieldValueInfo.java +++ b/src/main/java/graphql/execution/FieldValueInfo.java @@ -10,6 +10,16 @@ import static graphql.Assert.assertNotNull; +/** + * The {@link FieldValueInfo} holds the type of field that was fetched and completed along with the completed value. + *

+ * A field value is considered when its is both fetch via a {@link graphql.schema.DataFetcher} to a raw value, and then + * it is serialized into scalar or enum or if it's an object type, it is completed as an object given its field sub selection + *

+ * The {@link #getFieldValueObject()} method returns either a materialized value or a {@link CompletableFuture} + * promise to a materialized value. Simple in-memory values will tend to be materialized, while complicated + * values might need a call to a database or other systems will tend to be {@link CompletableFuture} promises. + */ @PublicApi public class FieldValueInfo { @@ -19,7 +29,6 @@ public enum CompleteValueType { NULL, SCALAR, ENUM - } private final CompleteValueType completeValueType; @@ -31,33 +40,64 @@ public FieldValueInfo(CompleteValueType completeValueType, Object fieldValueObje } public FieldValueInfo(CompleteValueType completeValueType, Object fieldValueObject, List fieldValueInfos) { - assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null"); + assertNotNull(fieldValueInfos, "fieldValueInfos can't be null"); this.completeValueType = completeValueType; this.fieldValueObject = fieldValueObject; this.fieldValueInfos = fieldValueInfos; } + /** + * This is an enum that represents the type of field value that was completed for a field + * + * @return the type of field value + */ public CompleteValueType getCompleteValueType() { return completeValueType; } - @Deprecated(since = "2023-09-11") - public CompletableFuture getFieldValue() { - return getFieldValueFuture().thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()); + /** + * This value can be either an object that is materialized or a {@link CompletableFuture} promise to a value + * + * @return either an object that is materialized or a {@link CompletableFuture} promise to a value + */ + public Object /* CompletableFuture | Object */ getFieldValueObject() { + return fieldValueObject; } + /** + * This returns the value in {@link CompletableFuture} form. If it is already a {@link CompletableFuture} it is returned + * directly, otherwise the materialized value is wrapped in a {@link CompletableFuture} and returned + * + * @return a {@link CompletableFuture} promise to the value + */ public CompletableFuture getFieldValueFuture() { return Async.toCompletableFuture(fieldValueObject); } - public Object /* CompletableFuture | Object */ getFieldValueObject() { - return fieldValueObject; + /** + * Kept for legacy reasons - this method is no longer sensible and is no longer used by the graphql-java engine + * and is kept only for backwards compatible API reasons. + * + * @return a promise to the {@link ExecutionResult} that wraps the field value. + */ + @Deprecated(since = "2023-09-11") + public CompletableFuture getFieldValue() { + return getFieldValueFuture().thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()); } + /** + * @return true if the value is a {@link CompletableFuture} promise to a value + */ public boolean isFutureValue() { return fieldValueObject instanceof CompletableFuture; } + /** + * When the {@link #getCompleteValueType()} is {@link CompleteValueType#LIST} this holds the list + * of completed values inside that list object. + * + * @return the list of completed field values inside a list + */ public List getFieldValueInfos() { return fieldValueInfos; } diff --git a/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java index ba1587c0cf..034138d110 100644 --- a/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java +++ b/src/main/java/graphql/execution/incremental/DeferredExecutionSupport.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; import graphql.Internal; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; @@ -162,7 +163,10 @@ private Supplier executionResultCF = fieldValueResult - .thenCompose(FieldValueInfo::getFieldValue); + .thenCompose(fvi -> fvi + .getFieldValueFuture() + .thenApply(fv -> ExecutionResultImpl.newExecutionResult().data(fv).build()) + ); return executionResultCF .thenApply(executionResult -> diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy index 0f1e17e715..6a3d72ec07 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy @@ -165,10 +165,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == result + executionResult == result } def "completes value for java.util.Optional"() { @@ -186,10 +186,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == expected + executionResult == expected where: result || expected @@ -212,7 +212,7 @@ class ExecutionStrategyTest extends Specification { .build() when: - executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: def e = thrown(CompletionException) @@ -234,10 +234,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == expected + executionResult == expected where: result || expected @@ -260,7 +260,7 @@ class ExecutionStrategyTest extends Specification { .build() when: - executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: def e = thrown(CompletionException) @@ -282,10 +282,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == expected + executionResult == expected where: result || expected @@ -308,7 +308,7 @@ class ExecutionStrategyTest extends Specification { .build() when: - executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: def e = thrown(CompletionException) @@ -330,10 +330,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == expected + executionResult == expected where: result || expected @@ -356,7 +356,7 @@ class ExecutionStrategyTest extends Specification { .build() when: - executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: def e = thrown(CompletionException) @@ -380,10 +380,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == result + executionResult == result } def "completing value with serializing throwing exception"() { @@ -402,10 +402,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == null + executionResult == null executionContext.errors.size() == 1 executionContext.errors[0] instanceof SerializationError @@ -427,10 +427,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == null + executionResult == null executionContext.errors.size() == 1 executionContext.errors[0] instanceof SerializationError @@ -787,10 +787,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.get().data == [1, 2, 3] + executionResult == [1, 2, 3] } def "#842 completes value for java.util.Stream"() { @@ -811,10 +811,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.get().data == [1, 2, 3] + executionResult == [1, 2, 3] } def "#842 completes value for java.util.Iterator"() { @@ -835,10 +835,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.get().data == [1, 2, 3] + executionResult == [1, 2, 3] } @@ -941,10 +941,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.get().data == [1L, 2L, 3L] + executionResult == [1L, 2L, 3L] } def "when completeValue expects GraphQLList and non iterable or non array is passed then it should yield a TypeMismatch error"() { @@ -965,10 +965,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue.join() + def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValueFuture.join() then: - executionResult.data == null + executionResult == null executionContext.errors.size() == 1 executionContext.errors[0] instanceof TypeMismatchError } From 734525bb8a9f6d7486bb842125ed0b503ee81449 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 28 Mar 2024 20:43:02 +1100 Subject: [PATCH 365/393] layout and made common check code for max nodes --- .../graphql/execution/ExecutionStrategy.java | 70 ++++++++++++------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index e50ba6fcb9..a0295e83be 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -196,7 +196,8 @@ public static String mkNameForPath(List currentField) { * @throws NonNullableFieldWasNullException in the {@link CompletableFuture} if a non-null field resolved to a null value */ @SuppressWarnings("unchecked") - protected Object /* CompletableFuture> | Map */ executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + protected Object /* CompletableFuture> | Map */ + executeObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = executionContext.getDataLoaderDispatcherStrategy(); dataLoaderDispatcherStrategy.executeObject(executionContext, parameters); Instrumentation instrumentation = executionContext.getInstrumentation(); @@ -354,7 +355,8 @@ Async.CombinedBuilder getAsyncFieldValueInfo( * @throws NonNullableFieldWasNullException in the future if a non-null field resolved to a null value */ @SuppressWarnings("unchecked") - protected Object /* CompletableFuture | Object */ resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + protected Object /* CompletableFuture | Object */ + resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { Object fieldWithInfo = resolveFieldWithInfo(executionContext, parameters); if (fieldWithInfo instanceof CompletableFuture) { return ((CompletableFuture) fieldWithInfo).thenCompose(FieldValueInfo::getFieldValueFuture); @@ -381,7 +383,8 @@ Async.CombinedBuilder getAsyncFieldValueInfo( * if a nonnull field resolves to a null value */ @SuppressWarnings("unchecked") - protected Object /* CompletableFuture | FieldValueInfo */ resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + protected Object /* CompletableFuture | FieldValueInfo */ + resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); @@ -426,23 +429,19 @@ Async.CombinedBuilder getAsyncFieldValueInfo( * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ - protected Object /*CompletableFuture | FetchedValue>*/ fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + protected Object /*CompletableFuture | FetchedValue>*/ + fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { MergedField field = parameters.getField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField()); return fetchField(fieldDef, executionContext, parameters); } - private Object /*CompletableFuture | FetchedValue>*/ fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + private Object /*CompletableFuture | FetchedValue>*/ + fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount(); - - Integer maxNodes; - if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { - if (resultNodesCount > maxNodes) { - executionContext.getResultNodesInfo().maxResultNodesExceeded(); - return CompletableFuture.completedFuture(new FetchedValue(null, Collections.emptyList(), null)); - } + if (incrementAndCheckMaxNodesExceeded(executionContext)) { + return new FetchedValue(null, Collections.emptyList(), null); } MergedField field = parameters.getField(); @@ -737,7 +736,8 @@ private FieldValueInfo getFieldValueInfoForNull(ExecutionStrategyParameters para * * @throws NonNullableFieldWasNullException inside the {@link CompletableFuture} if a non-null field resolves to a null value */ - protected Object /* CompletableFuture | Object */ completeValueForNull(ExecutionStrategyParameters parameters) { + protected Object /* CompletableFuture | Object */ + completeValueForNull(ExecutionStrategyParameters parameters) { try { return parameters.getNonNullFieldValidator().validate(parameters.getPath(), null); } catch (Exception e) { @@ -793,13 +793,8 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, List fieldValueInfos = new ArrayList<>(size.orElse(1)); int index = 0; for (Object item : iterableValues) { - int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount(); - Integer maxNodes; - if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { - if (resultNodesCount > maxNodes) { - executionContext.getResultNodesInfo().maxResultNodesExceeded(); - return new FieldValueInfo(NULL, null, fieldValueInfos); - } + if (incrementAndCheckMaxNodesExceeded(executionContext)) { + return new FieldValueInfo(NULL, null, fieldValueInfos); } ResultPath indexedPath = parameters.getPath().segment(index); @@ -880,7 +875,8 @@ protected void handleValueException(CompletableFuture overallResult, Thro * * @return a materialized scalar value or exceptionally completed {@link CompletableFuture} if there is a problem */ - protected Object /* CompletableFuture | Object */ completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { + protected Object /* CompletableFuture | Object */ + completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { Object serialized; try { serialized = scalarType.getCoercing().serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); @@ -906,7 +902,8 @@ protected void handleValueException(CompletableFuture overallResult, Thro * * @return a materialized enum value or exceptionally completed {@link CompletableFuture} if there is a problem */ - protected Object /* CompletableFuture | Object */ completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { + protected Object /* CompletableFuture | Object */ + completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { Object serialized; try { serialized = enumType.serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); @@ -931,7 +928,8 @@ protected void handleValueException(CompletableFuture overallResult, Thro * * @return a {@link CompletableFuture} promise to a map of object field values or a materialized map of object field values */ - protected Object /* CompletableFuture> | Map */ completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { + protected Object /* CompletableFuture> | Map */ + completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); FieldCollectorParameters collectorParameters = newParameters() @@ -987,17 +985,37 @@ protected Iterable toIterable(ExecutionContext context, ExecutionStrateg return FpKit.toIterable(result); } - handleTypeMismatchProblem(context, parameters, result); + handleTypeMismatchProblem(context, parameters); return null; } - private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) { + private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrategyParameters parameters) { TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getExecutionStepInfo().getUnwrappedNonNullType()); context.addError(error); parameters.getDeferredCallContext().onError(error); } + /** + * This has a side effect of incrementing the number of nodes returned and also checks + * if max nodes were exceeded for this request. + * + * @param executionContext the execution context in play + * + * @return true if max nodes were exceeded + */ + private boolean incrementAndCheckMaxNodesExceeded(ExecutionContext executionContext) { + int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount(); + Integer maxNodes; + if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) { + if (resultNodesCount > maxNodes) { + executionContext.getResultNodesInfo().maxResultNodesExceeded(); + return true; + } + } + return false; + } + /** * Called to discover the field definition give the current parameters and the AST {@link Field} * From a7451cb92ae23a1925948861c192731ca2e05aba Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 28 Mar 2024 21:01:32 +1100 Subject: [PATCH 366/393] fixed agent contract and jaavdoc in one file --- src/main/java/graphql/execution/ExecutionStrategy.java | 8 ++++++-- .../normalized/ExecutableNormalizedOperationFactory.java | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index a0295e83be..5e749360a5 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -493,7 +493,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); dataFetcher = executionContext.getDataLoaderDispatcherStrategy().modifyDataFetcher(dataFetcher); - Object fetchedObject = invokeDataFetcher(parameters, fieldDef, dataFetchingEnvironment, dataFetcher); + Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject); fetchCtx.onDispatched(); if (fetchedObject instanceof CompletableFuture) { @@ -517,7 +517,11 @@ Async.CombinedBuilder getAsyncFieldValueInfo( } } - private Object invokeDataFetcher(ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { + /* + * ExecutionContext is not used in the method, but the java agent uses it, so it needs to be present + */ + @SuppressWarnings("unused") + private Object invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { Object fetchedValue; try { Object fetchedValueRaw; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 161ed526f6..085fad0afb 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -34,7 +34,6 @@ import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; import graphql.schema.GraphQLObjectType; @@ -326,6 +325,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param operationDefinition the operation to be executed * @param fragments a set of fragments associated with the operation * @param coercedVariableValues the coerced variables to use + * @param options the options to use * * @return a runtime representation of the graphql operation. */ From d843a3004f9e5b0e8e24b977c1843abb866b5c9e Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Sun, 31 Mar 2024 11:50:46 -0700 Subject: [PATCH 367/393] validate non-nullable directive args --- .../AppliedDirectiveArgumentsAreValid.java | 53 +++++++++++++------ .../graphql/schema/GraphQLArgumentTest.groovy | 52 ++++++++++++++---- 2 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java b/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java index 82e59e2785..42591a7c46 100644 --- a/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java +++ b/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java @@ -5,14 +5,8 @@ import graphql.execution.NonNullableValueCoercedAsNullException; import graphql.execution.ValuesResolver; import graphql.language.Value; -import graphql.schema.CoercingParseValueException; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLDirective; -import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLSchema; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLTypeVisitorStub; -import graphql.schema.InputValueWithState; +import graphql.schema.*; +import graphql.schema.idl.TypeUtil; import graphql.util.TraversalControl; import graphql.util.TraverserContext; import graphql.validation.ValidationUtil; @@ -32,29 +26,56 @@ public TraversalControl visitGraphQLDirective(GraphQLDirective directive, Traver // if there is no parent it means it is just a directive definition and not an applied directive if (context.getParentNode() != null) { for (GraphQLArgument graphQLArgument : directive.getArguments()) { - checkArgument(directive, graphQLArgument, context); + checkArgument( + directive.getName(), + graphQLArgument.getName(), + graphQLArgument.getArgumentValue(), + graphQLArgument.getType(), + context + ); } } return TraversalControl.CONTINUE; } - private void checkArgument(GraphQLDirective directive, GraphQLArgument argument, TraverserContext context) { - if (!argument.hasSetValue()) { - return; + @Override + public TraversalControl visitGraphQLAppliedDirective(GraphQLAppliedDirective directive, TraverserContext context) { + // if there is no parent it means it is just a directive definition and not an applied directive + if (context.getParentNode() != null) { + for (GraphQLAppliedDirectiveArgument graphQLArgument : directive.getArguments()) { + checkArgument( + directive.getName(), + graphQLArgument.getName(), + graphQLArgument.getArgumentValue(), + graphQLArgument.getType(), + context + ); + } } + return TraversalControl.CONTINUE; + } + + private void checkArgument( + String directiveName, + String argumentName, + InputValueWithState argumentValue, + GraphQLInputType argumentType, + TraverserContext context + ) { GraphQLSchema schema = context.getVarFromParents(GraphQLSchema.class); SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); - InputValueWithState argumentValue = argument.getArgumentValue(); boolean invalid = false; if (argumentValue.isLiteral() && - !validationUtil.isValidLiteralValue((Value) argumentValue.getValue(), argument.getType(), schema, GraphQLContext.getDefault(), Locale.getDefault())) { + !validationUtil.isValidLiteralValue((Value) argumentValue.getValue(), argumentType, schema, GraphQLContext.getDefault(), Locale.getDefault())) { invalid = true; } else if (argumentValue.isExternal() && - !isValidExternalValue(schema, argumentValue.getValue(), argument.getType(), GraphQLContext.getDefault(), Locale.getDefault())) { + !isValidExternalValue(schema, argumentValue.getValue(), argumentType, GraphQLContext.getDefault(), Locale.getDefault())) { + invalid = true; + } else if (argumentValue.isNotSet() && GraphQLTypeUtil.isNonNull(argumentType)) { invalid = true; } if (invalid) { - String message = format("Invalid argument '%s' for applied directive of name '%s'", argument.getName(), directive.getName()); + String message = format("Invalid argument '%s' for applied directive of name '%s'", argumentName, directiveName); errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.InvalidAppliedDirectiveArgument, message)); } } diff --git a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy index 23a506621d..c39d280fe7 100644 --- a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy @@ -195,23 +195,53 @@ class GraphQLArgumentTest extends Specification { resolvedDefaultValue == null } - def "Applied schema directives arguments are validated for programmatic schemas"() { + def "schema directive arguments are validated for programmatic schemas"() { given: def arg = newArgument().name("arg").type(GraphQLInt).valueProgrammatic(ImmutableKit.emptyMap()).build() // Retain for test coverage - def directive = GraphQLDirective.newDirective().name("cached").argument(arg).build() + def directive = newDirective().name("cached").argument(arg).build() def field = newFieldDefinition() - .name("hello") - .type(GraphQLString) - .argument(arg) - .withDirective(directive) - .build() + .name("hello") + .type(GraphQLString) + .argument(arg) + .withDirective(directive) + .build() when: - newSchema().query( + newSchema() + .query( newObject() - .name("Query") - .field(field) - .build()) + .name("Query") + .field(field) + .build() + ) + .additionalDirective(directive) + .build() + then: + def e = thrown(InvalidSchemaException) + e.message.contains("Invalid argument 'arg' for applied directive of name 'cached'") + } + + def "applied directive arguments are validated for programmatic schemas"() { + given: + def arg = newArgument() + .name("arg") + .type(GraphQLNonNull.nonNull(GraphQLInt)) .build() + def directive = newDirective().name("cached").argument(arg).build() + def field = newFieldDefinition() + .name("hello") + .type(GraphQLString) + .withAppliedDirective(directive.toAppliedDirective()) + .build() + when: + newSchema() + .query( + newObject() + .name("Query") + .field(field) + .build() + ) + .additionalDirective(directive) + .build() then: def e = thrown(InvalidSchemaException) e.message.contains("Invalid argument 'arg' for applied directive of name 'cached'") From 897d8d41de576dd82a178e4b1409983a0d23c1ef Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Sun, 31 Mar 2024 13:02:45 -0700 Subject: [PATCH 368/393] require non-empty directive locations --- .../java/graphql/schema/GraphQLDirective.java | 4 +- src/test/groovy/graphql/TestUtil.groovy | 41 ++++++------------ .../graphql/schema/GraphQLArgumentTest.groovy | 23 +++++----- .../schema/GraphQLDirectiveTest.groovy | 31 ++++++++++++++ .../GraphQLEnumValueDefinitionTest.groovy | 8 ++-- .../schema/GraphQLFieldDefinitionTest.groovy | 12 +++--- .../schema/GraphQLInputObjectFieldTest.groovy | 12 +++--- .../schema/GraphQLScalarTypeTest.groovy | 10 ++--- .../graphql/schema/SchemaTraverserTest.groovy | 42 ++++++------------- .../schema/idl/SchemaGeneratorTest.groovy | 3 +- .../rules/KnownArgumentNamesTest.groovy | 13 ++++-- .../rules/ProvidedNonNullArgumentsTest.groovy | 4 ++ 12 files changed, 104 insertions(+), 99 deletions(-) diff --git a/src/main/java/graphql/schema/GraphQLDirective.java b/src/main/java/graphql/schema/GraphQLDirective.java index 1dbc41042c..d0033804c9 100644 --- a/src/main/java/graphql/schema/GraphQLDirective.java +++ b/src/main/java/graphql/schema/GraphQLDirective.java @@ -15,8 +15,7 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; -import static graphql.Assert.assertNotNull; -import static graphql.Assert.assertValidName; +import static graphql.Assert.*; import static graphql.introspection.Introspection.DirectiveLocation; import static graphql.util.FpKit.getByName; @@ -52,6 +51,7 @@ private GraphQLDirective(String name, DirectiveDefinition definition) { assertValidName(name); assertNotNull(arguments, () -> "arguments can't be null"); + assertNotEmpty(locations, () -> "locations can't be empty"); this.name = name; this.description = description; this.repeatable = repeatable; diff --git a/src/test/groovy/graphql/TestUtil.groovy b/src/test/groovy/graphql/TestUtil.groovy index 35a2ae68b2..fb677bd21b 100644 --- a/src/test/groovy/graphql/TestUtil.groovy +++ b/src/test/groovy/graphql/TestUtil.groovy @@ -2,32 +2,11 @@ package graphql import graphql.execution.MergedField import graphql.execution.MergedSelectionSet -import graphql.language.Document -import graphql.language.Field -import graphql.language.NullValue -import graphql.language.ObjectTypeDefinition -import graphql.language.OperationDefinition -import graphql.language.ScalarTypeDefinition -import graphql.language.Type +import graphql.introspection.Introspection.DirectiveLocation +import graphql.language.* import graphql.parser.Parser -import graphql.schema.Coercing -import graphql.schema.DataFetcher -import graphql.schema.GraphQLAppliedDirectiveArgument -import graphql.schema.GraphQLAppliedDirective -import graphql.schema.GraphQLArgument -import graphql.schema.GraphQLDirective -import graphql.schema.GraphQLInputType -import graphql.schema.GraphQLObjectType -import graphql.schema.GraphQLScalarType -import graphql.schema.GraphQLSchema -import graphql.schema.GraphQLType -import graphql.schema.TypeResolver -import graphql.schema.idl.RuntimeWiring -import graphql.schema.idl.SchemaGenerator -import graphql.schema.idl.SchemaParser -import graphql.schema.idl.TestMockedWiringFactory -import graphql.schema.idl.TypeRuntimeWiring -import graphql.schema.idl.WiringFactory +import graphql.schema.* +import graphql.schema.idl.* import graphql.schema.idl.errors.SchemaProblem import groovy.json.JsonOutput @@ -194,13 +173,19 @@ class TestUtil { .name(definition.getName()) .description(definition.getDescription() == null ? null : definition.getDescription().getContent()) .coercing(mockCoercing()) - .replaceDirectives(definition.getDirectives().stream().map({ mockDirective(it.getName()) }).collect(Collectors.toList())) + .replaceDirectives( + definition.getDirectives() + .stream() + .map({ mockDirective(it.getName(), DirectiveLocation.SCALAR) }) + .collect(Collectors.toList())) .definition(definition) .build() } - static GraphQLDirective mockDirective(String name) { - newDirective().name(name).description(name).build() + static GraphQLDirective mockDirective(String name, DirectiveLocation location, GraphQLArgument arg = null) { + def b = newDirective().name(name).description(name).validLocation(location) + if (arg != null) b.argument(arg) + b.build() } static TypeRuntimeWiring mockTypeRuntimeWiring(String typeName, boolean withResolver) { diff --git a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy index 23a506621d..5b977eb215 100644 --- a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy @@ -1,6 +1,7 @@ package graphql.schema import graphql.collect.ImmutableKit +import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION import graphql.language.FloatValue import graphql.schema.validation.InvalidSchemaException import spock.lang.Specification @@ -9,10 +10,10 @@ import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString import static graphql.schema.GraphQLArgument.newArgument -import static graphql.schema.GraphQLDirective.newDirective import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition import static graphql.schema.GraphQLObjectType.newObject import static graphql.schema.GraphQLSchema.newSchema +import static graphql.TestUtil.mockDirective class GraphQLArgumentTest extends Specification { @@ -22,7 +23,7 @@ class GraphQLArgumentTest extends Specification { .description("A1_description") .type(GraphQLInt) .deprecate("custom reason") - .withDirective(newDirective().name("directive1")) + .withDirective(mockDirective("directive1", ARGUMENT_DEFINITION)) .build() when: def transformedArgument = startingArgument.transform({ @@ -30,7 +31,7 @@ class GraphQLArgumentTest extends Specification { .name("A2") .description("A2_description") .type(GraphQLString) - .withDirective(newDirective().name("directive3")) + .withDirective(mockDirective("directive3", ARGUMENT_DEFINITION)) .value("VALUE") // Retain deprecated for test coverage .deprecate(null) .defaultValue("DEFAULT") // Retain deprecated for test coverage @@ -79,9 +80,9 @@ class GraphQLArgumentTest extends Specification { def argument given: - def builder = GraphQLArgument.newArgument().name("A1") + def builder = newArgument().name("A1") .type(GraphQLInt) - .withDirective(newDirective().name("directive1")) + .withDirective(mockDirective("directive1", ARGUMENT_DEFINITION)) when: argument = builder.build() @@ -96,8 +97,8 @@ class GraphQLArgumentTest extends Specification { when: argument = builder .clearDirectives() - .withDirective(newDirective().name("directive2")) - .withDirective(newDirective().name("directive3")) + .withDirective(mockDirective("directive2", ARGUMENT_DEFINITION)) + .withDirective(mockDirective("directive3", ARGUMENT_DEFINITION)) .build() then: @@ -109,9 +110,9 @@ class GraphQLArgumentTest extends Specification { when: argument = builder .replaceDirectives([ - newDirective().name("directive1").build(), - newDirective().name("directive2").build(), - newDirective().name("directive3").build()]) // overwrite + mockDirective("directive1", ARGUMENT_DEFINITION), + mockDirective("directive2", ARGUMENT_DEFINITION), + mockDirective("directive3", ARGUMENT_DEFINITION)]) // overwrite .build() then: @@ -198,7 +199,7 @@ class GraphQLArgumentTest extends Specification { def "Applied schema directives arguments are validated for programmatic schemas"() { given: def arg = newArgument().name("arg").type(GraphQLInt).valueProgrammatic(ImmutableKit.emptyMap()).build() // Retain for test coverage - def directive = GraphQLDirective.newDirective().name("cached").argument(arg).build() + def directive = mockDirective("cached", ARGUMENT_DEFINITION, arg) def field = newFieldDefinition() .name("hello") .type(GraphQLString) diff --git a/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy b/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy index 5eba0fbc3a..4c9b8bd486 100644 --- a/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy @@ -1,6 +1,8 @@ package graphql.schema +import graphql.AssertException import graphql.TestUtil +import graphql.introspection.Introspection import graphql.language.Node import spock.lang.Specification @@ -168,9 +170,38 @@ class GraphQLDirectiveTest extends Specification { then: assertDirectiveContainer(scalarType) + } + + def "throws an error on missing required properties"() { + given: + def validDirective = GraphQLDirective.newDirective() + .name("dir") + .validLocation(Introspection.DirectiveLocation.SCALAR) + .build() + + when: + validDirective.transform { it.name(null) } + + then: + def e = thrown(AssertException) + e.message.contains("Name must be non-null, non-empty") + when: + validDirective.transform { it.replaceArguments(null) } + + then: + def e2 = thrown(AssertException) + e2.message.contains("arguments must not be null") + + when: + validDirective.transform { it.clearValidLocations() } + + then: + def e3 = thrown(AssertException) + e3.message.contains("locations can't be empty") } + static boolean assertDirectiveContainer(GraphQLDirectiveContainer container) { assert container.hasDirective("d1") // Retain for test coverage assert container.hasAppliedDirective("d1") diff --git a/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy b/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy index 8f6a4145bc..216689d457 100644 --- a/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy @@ -1,9 +1,10 @@ package graphql.schema +import static graphql.introspection.Introspection.DirectiveLocation import spock.lang.Specification -import static graphql.schema.GraphQLDirective.newDirective import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition +import static graphql.TestUtil.mockDirective class GraphQLEnumValueDefinitionTest extends Specification { def "object can be transformed"() { @@ -11,15 +12,14 @@ class GraphQLEnumValueDefinitionTest extends Specification { def startEnumValue = newEnumValueDefinition().name("EV1") .description("EV1_description") .value("A") - .withDirective(newDirective().name("directive1")) + .withDirective(mockDirective("directive1", DirectiveLocation.ENUM_VALUE)) .build() when: def transformedEnumValue = startEnumValue.transform({ it .name("EV2") .value("X") - .withDirective(newDirective().name("directive2")) - + .withDirective(mockDirective("directive2", DirectiveLocation.ENUM_VALUE)) }) then: diff --git a/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy b/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy index d3c775cc9e..8e578a4a96 100644 --- a/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy @@ -2,6 +2,7 @@ package graphql.schema import graphql.AssertException import graphql.TestUtil +import graphql.introspection.Introspection import graphql.schema.idl.SchemaPrinter import spock.lang.Specification @@ -10,9 +11,9 @@ import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString import static graphql.TestUtil.mockArguments +import static graphql.TestUtil.mockDirective import static graphql.schema.DefaultGraphqlTypeComparatorRegistry.newComparators import static graphql.schema.GraphQLArgument.newArgument -import static graphql.schema.GraphQLDirective.newDirective import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition import static graphql.schema.idl.SchemaPrinter.Options.defaultOptions @@ -35,8 +36,8 @@ class GraphQLFieldDefinitionTest extends Specification { .deprecate("F1_deprecated") .argument(newArgument().name("argStr").type(GraphQLString)) .argument(newArgument().name("argInt").type(GraphQLInt)) - .withDirective(newDirective().name("directive1")) - .withDirective(newDirective().name("directive2")) + .withDirective(mockDirective("directive1", Introspection.DirectiveLocation.FIELD_DEFINITION)) + .withDirective(mockDirective("directive2", Introspection.DirectiveLocation.FIELD_DEFINITION)) .build() when: @@ -47,13 +48,10 @@ class GraphQLFieldDefinitionTest extends Specification { .argument(newArgument().name("argStr").type(GraphQLString)) .argument(newArgument().name("argInt").type(GraphQLBoolean)) .argument(newArgument().name("argIntAdded").type(GraphQLInt)) - .withDirective(newDirective().name("directive3")) - + .withDirective(mockDirective("directive3", Introspection.DirectiveLocation.FIELD_DEFINITION)) }) - then: - startingField.name == "F1" startingField.type == GraphQLFloat startingField.description == "F1_description" diff --git a/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy b/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy index 27d9fe8da9..62bf92f6df 100644 --- a/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy @@ -1,12 +1,13 @@ package graphql.schema +import graphql.introspection.Introspection import graphql.language.FloatValue import spock.lang.Specification import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLInt -import static graphql.schema.GraphQLDirective.newDirective import static graphql.schema.GraphQLInputObjectField.newInputObjectField +import static graphql.TestUtil.mockDirective class GraphQLInputObjectFieldTest extends Specification { @@ -16,8 +17,8 @@ class GraphQLInputObjectFieldTest extends Specification { .name("F1") .type(GraphQLFloat) .description("F1_description") - .withDirective(newDirective().name("directive1")) - .withDirective(newDirective().name("directive2")) + .withDirective(mockDirective("directive1", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) + .withDirective(mockDirective("directive2", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) .deprecate("No longer useful") .build() @@ -26,13 +27,10 @@ class GraphQLInputObjectFieldTest extends Specification { builder.name("F2") .type(GraphQLInt) .deprecate(null) - .withDirective(newDirective().name("directive3")) - + .withDirective(mockDirective("directive3", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) }) - then: - startingField.name == "F1" startingField.type == GraphQLFloat startingField.description == "F1_description" diff --git a/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy b/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy index ca020cb0b5..b901ddc47c 100644 --- a/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy @@ -1,8 +1,9 @@ package graphql.schema +import graphql.introspection.Introspection import spock.lang.Specification -import static graphql.schema.GraphQLDirective.newDirective +import static graphql.TestUtil.mockDirective class GraphQLScalarTypeTest extends Specification { Coercing coercing = new Coercing() { @@ -28,14 +29,14 @@ class GraphQLScalarTypeTest extends Specification { .name("S1") .description("S1_description") .coercing(coercing) - .withDirective(newDirective().name("directive1")) - .withDirective(newDirective().name("directive2")) + .withDirective(mockDirective("directive1", Introspection.DirectiveLocation.SCALAR)) + .withDirective(mockDirective("directive2", Introspection.DirectiveLocation.SCALAR)) .build() when: def transformedScalar = startingScalar.transform({ builder -> builder.name("S2") .description("S2_description") - .withDirective(newDirective().name("directive3")) + .withDirective(mockDirective("directive3", Introspection.DirectiveLocation.SCALAR)) }) then: @@ -55,6 +56,5 @@ class GraphQLScalarTypeTest extends Specification { transformedScalar.getDirective("directive1") != null transformedScalar.getDirective("directive2") != null transformedScalar.getDirective("directive3") != null - } } diff --git a/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy b/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy index 10a8d53a93..ce2a523e21 100644 --- a/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy @@ -6,25 +6,22 @@ import graphql.util.TraversalControl import graphql.util.TraverserContext import spock.lang.Specification +import static graphql.introspection.Introspection.DirectiveLocation import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLTypeReference.typeRef import static graphql.schema.GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY +import static graphql.TestUtil.mockDirective class SchemaTraverserTest extends Specification { - def "reachable scalar type"() { when: - def visitor = new GraphQLTestingVisitor() new SchemaTraverser().depthFirst(visitor, Scalars.GraphQLString) then: - visitor.getStack() == ["scalar: String", "fallback: String"] - - } def "reachable string argument type"() { @@ -48,7 +45,6 @@ class SchemaTraverserTest extends Specification { .build()) then: visitor.getStack() == ["argument: Test", "fallback: Test", "scalar: Int", "fallback: Int"] - } def "reachable enum type"() { @@ -65,7 +61,6 @@ class SchemaTraverserTest extends Specification { visitor.getStack() == ["enum: foo", "fallback: foo", "enum value: abc", "fallback: abc", "enum value: bar", "fallback: bar"] - } def "reachable field definition type"() { @@ -77,7 +72,6 @@ class SchemaTraverserTest extends Specification { .build()) then: visitor.getStack() == ["field: foo", "fallback: foo", "scalar: String", "fallback: String"] - } def "reachable input object field type"() { @@ -107,7 +101,6 @@ class SchemaTraverserTest extends Specification { "scalar: String", "fallback: String"] } - def "reachable interface type"() { when: def visitor = new GraphQLTestingVisitor() @@ -163,7 +156,6 @@ class SchemaTraverserTest extends Specification { "interface: bar", "fallback: bar"] } - def "reachable reference type"() { when: def visitor = new GraphQLTestingVisitor() @@ -210,8 +202,7 @@ class SchemaTraverserTest extends Specification { def scalarType = GraphQLScalarType.newScalar() .name("foo") .coercing(coercing) - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.SCALAR)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -227,8 +218,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def objectType = GraphQLObjectType.newObject() .name("foo") - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.OBJECT)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -245,8 +235,7 @@ class SchemaTraverserTest extends Specification { def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition() .name("foo") .type(Scalars.GraphQLString) - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.FIELD_DEFINITION)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -263,8 +252,7 @@ class SchemaTraverserTest extends Specification { def argument = newArgument() .name("foo") .type(Scalars.GraphQLString) - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.ARGUMENT_DEFINITION)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -280,8 +268,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def interfaceType = GraphQLInterfaceType.newInterface() .name("foo") - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.INTERFACE)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -298,8 +285,7 @@ class SchemaTraverserTest extends Specification { def unionType = GraphQLUnionType.newUnionType() .name("foo") .possibleType(GraphQLObjectType.newObject().name("dummy").build()) - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.UNION)) .build() new SchemaTraverser().depthFirst(visitor, unionType) then: @@ -312,8 +298,7 @@ class SchemaTraverserTest extends Specification { def enumType = GraphQLEnumType.newEnum() .name("foo") .value("dummy") - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.ENUM)) .build() new SchemaTraverser().depthFirst(visitor, enumType) then: @@ -325,8 +310,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def enumValue = GraphQLEnumValueDefinition.newEnumValueDefinition() .name("foo") - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.ENUM_VALUE)) .build() new SchemaTraverser().depthFirst(visitor, enumValue) then: @@ -338,8 +322,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def inputObjectType = GraphQLInputObjectType.newInputObject() .name("foo") - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.INPUT_OBJECT)) .build() new SchemaTraverser().depthFirst(visitor, inputObjectType) then: @@ -352,8 +335,7 @@ class SchemaTraverserTest extends Specification { def inputField = GraphQLInputObjectField.newInputObjectField() .name("foo") .type(Scalars.GraphQLString) - .withDirective(GraphQLDirective.newDirective() - .name("bar")) + .withDirective(mockDirective("bar", DirectiveLocation.INPUT_FIELD_DEFINITION)) .build() new SchemaTraverser().depthFirst(visitor, inputField) then: diff --git a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy index e24317e6fe..ba572a7f8c 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy @@ -1,6 +1,5 @@ package graphql.schema.idl - import graphql.TestUtil import graphql.introspection.Introspection import graphql.language.Node @@ -11,7 +10,6 @@ import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLAppliedDirective import graphql.schema.GraphQLArgument import graphql.schema.GraphQLCodeRegistry -import graphql.schema.GraphQLDirective import graphql.schema.GraphQLDirectiveContainer import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLFieldDefinition @@ -44,6 +42,7 @@ import static graphql.language.AstPrinter.printAst import static graphql.schema.GraphQLCodeRegistry.newCodeRegistry import static graphql.schema.idl.SchemaGenerator.Options.defaultOptions import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring +import static graphql.TestUtil.mockDirective class SchemaGeneratorTest extends Specification { diff --git a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy index 302f34b7a1..e437b43eda 100644 --- a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy @@ -1,5 +1,6 @@ package graphql.validation.rules +import graphql.introspection.Introspection import graphql.language.Argument import graphql.language.BooleanValue import graphql.language.StringValue @@ -52,7 +53,9 @@ class KnownArgumentNamesTest extends Specification { given: Argument argument = Argument.newArgument("unknownArg", BooleanValue.newBooleanValue(true).build()).build() def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString).build() - def directiveDefinition = GraphQLDirective.newDirective().name("directive") + def directiveDefinition = GraphQLDirective.newDirective() + .name("directive") + .validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION) .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLBoolean).build()).build() validationContext.getFieldDef() >> fieldDefinition validationContext.getDirective() >> directiveDefinition @@ -66,7 +69,9 @@ class KnownArgumentNamesTest extends Specification { given: Argument argument = Argument.newArgument("knownArg", BooleanValue.newBooleanValue(true).build()).build() def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString).build() - def directiveDefinition = GraphQLDirective.newDirective().name("directive") + def directiveDefinition = GraphQLDirective.newDirective() + .name("directive") + .validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION) .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLBoolean).build()).build() validationContext.getFieldDef() >> fieldDefinition validationContext.getDirective() >> directiveDefinition @@ -81,7 +86,9 @@ class KnownArgumentNamesTest extends Specification { Argument argument = Argument.newArgument("unknownArg", BooleanValue.newBooleanValue(true).build()).build() def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString) .argument(GraphQLArgument.newArgument().name("unknownArg").type(GraphQLString).build()).build() - def directiveDefinition = GraphQLDirective.newDirective().name("directive") + def directiveDefinition = GraphQLDirective.newDirective() + .name("directive") + .validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION) .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLBoolean).build()).build() validationContext.getFieldDef() >> fieldDefinition validationContext.getDirective() >> directiveDefinition diff --git a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy index 925f6603eb..9a11066973 100644 --- a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy @@ -1,5 +1,6 @@ package graphql.validation.rules +import graphql.introspection.Introspection import graphql.language.Argument import graphql.language.Directive import graphql.language.Field @@ -111,6 +112,7 @@ class ProvidedNonNullArgumentsTest extends Specification { .name("arg").type(GraphQLNonNull.nonNull(GraphQLString)) def graphQLDirective = GraphQLDirective.newDirective() .name("directive") + .validLocation(Introspection.DirectiveLocation.SCALAR) .argument(directiveArg) .build() validationContext.getDirective() >> graphQLDirective @@ -149,6 +151,7 @@ class ProvidedNonNullArgumentsTest extends Specification { .defaultValueProgrammatic("defaultVal") def graphQLDirective = GraphQLDirective.newDirective() .name("directive") + .validLocation(Introspection.DirectiveLocation.SCALAR) .argument(directiveArg) .build() validationContext.getDirective() >> graphQLDirective @@ -167,6 +170,7 @@ class ProvidedNonNullArgumentsTest extends Specification { def directiveArg = GraphQLArgument.newArgument().name("arg").type(GraphQLNonNull.nonNull(GraphQLString)) def graphQLDirective = GraphQLDirective.newDirective() .name("directive") + .validLocation(Introspection.DirectiveLocation.SCALAR) .argument(directiveArg) .build() validationContext.getDirective() >> graphQLDirective From 0dac3075c10090d0de33c90562b7f2a4967548c4 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Sun, 31 Mar 2024 19:40:15 -0700 Subject: [PATCH 369/393] remove wildcard import --- .../AppliedDirectiveArgumentsAreValid.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java b/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java index 42591a7c46..1eec8bff75 100644 --- a/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java +++ b/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java @@ -5,8 +5,17 @@ import graphql.execution.NonNullableValueCoercedAsNullException; import graphql.execution.ValuesResolver; import graphql.language.Value; -import graphql.schema.*; -import graphql.schema.idl.TypeUtil; +import graphql.schema.CoercingParseValueException; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeUtil; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.InputValueWithState; import graphql.util.TraversalControl; import graphql.util.TraverserContext; import graphql.validation.ValidationUtil; From da2bce12c2696cf7c795967c7d38dad55e373aa6 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:17:48 +1100 Subject: [PATCH 370/393] Add error strings --- src/main/resources/i18n/Scalars.properties | 4 ++++ src/main/resources/i18n/Scalars_de.properties | 4 ++++ src/main/resources/i18n/Scalars_nl.properties | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/src/main/resources/i18n/Scalars.properties b/src/main/resources/i18n/Scalars.properties index 0897fe58eb..85aa20b50f 100644 --- a/src/main/resources/i18n/Scalars.properties +++ b/src/main/resources/i18n/Scalars.properties @@ -24,6 +24,10 @@ ID.unexpectedAstType=Expected an AST type of ''IntValue'' or ''StringValue'' but # Float.notFloat=Expected a value that can be converted to type ''Float'' but it was a ''{0}'' Float.unexpectedAstType=Expected an AST type of ''IntValue'' or ''FloatValue'' but it was a ''{0}'' +Float.unexpectedRawValueType=Expected a Number input, but it was a ''{0}'' # Boolean.notBoolean=Expected a value that can be converted to type ''Boolean'' but it was a ''{0}'' Boolean.unexpectedAstType=Expected an AST type of ''BooleanValue'' but it was a ''{0}'' +Boolean.unexpectedRawValueType=Expected a Boolean input, but it was a ''{0}'' +# +String.unexpectedRawValueType=Expected a String input, but it was a ''{0}'' \ No newline at end of file diff --git a/src/main/resources/i18n/Scalars_de.properties b/src/main/resources/i18n/Scalars_de.properties index cb06a23669..8c82638362 100644 --- a/src/main/resources/i18n/Scalars_de.properties +++ b/src/main/resources/i18n/Scalars_de.properties @@ -24,6 +24,10 @@ ID.unexpectedAstType=Erwartet wurde ein AST type von ''IntValue'' oder ''StringV # Float.notFloat=Erwartet wurde ein Wert, der in den Typ ''Float'' konvertiert werden kann, aber es war ein ''{0}'' Float.unexpectedAstType=Erwartet wurde ein AST type von ''IntValue'' oder ''FloatValue'', aber es war ein ''{0}'' +Float.unexpectedRawValueType=Erwartet wurde eine Number-Eingabe, aber es war ein ''{0}'' # Boolean.notBoolean=Erwartet wurde ein Wert, der in den Typ ''Boolean'' konvertiert werden kann, aber es war ein ''{0}'' Boolean.unexpectedAstType=Erwartet wurde ein AST type ''BooleanValue'', aber es war ein ''{0}'' +Boolean.unexpectedRawValueType=Erwartet wurde eine Boolean-Eingabe, aber es war ein ''{0}'' +# +String.unexpectedRawValueType=Erwartet wurde eine String-Eingabe, aber es war ein ''{0}'' \ No newline at end of file diff --git a/src/main/resources/i18n/Scalars_nl.properties b/src/main/resources/i18n/Scalars_nl.properties index f12f10e811..9878c1716f 100644 --- a/src/main/resources/i18n/Scalars_nl.properties +++ b/src/main/resources/i18n/Scalars_nl.properties @@ -24,6 +24,13 @@ ID.unexpectedAstType=Verwacht werd een AST-type ''IntValue'' of ''StringValue'', # Float.notFloat=Verwacht werd een waarde die in ''Float'' veranderd kon worden, maar het was een ''{0}'' Float.unexpectedAstType=Verwacht werd een AST-type ''IntValue'' of ''FloatValue'', maar het was een ''{0}'' +# TODO: To be translated into Dutch +Float.unexpectedRawValueType=Expected a Number input, but it was a ''{0}'' # Boolean.notBoolean=Verwacht werd een waarde die in ''Boolean'' veranderd kon worden, maar het was een ''{0}'' Boolean.unexpectedAstType=Verwacht werd een AST-type ''BooleanValue'', maar het was een ''{0}'' +# TODO: To be translated into Dutch +Boolean.unexpectedRawValueType=Expected a Boolean input, but it was a ''{0}'' +# +# TODO: To be translated into Dutch +String.unexpectedRawValueType=Expected a String input, but it was a ''{0}'' From b7a648cc14c5ab96a7eeff0a6b56d55971ca8d94 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:33:31 +1100 Subject: [PATCH 371/393] Add stricter string parseValue coercion --- .../graphql/scalar/GraphqlStringCoercing.java | 15 +++++++++--- .../groovy/graphql/ScalarsStringTest.groovy | 24 +++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/main/java/graphql/scalar/GraphqlStringCoercing.java b/src/main/java/graphql/scalar/GraphqlStringCoercing.java index 9b0d6b84ae..9040cc03f8 100644 --- a/src/main/java/graphql/scalar/GraphqlStringCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlStringCoercing.java @@ -28,6 +28,15 @@ private String toStringImpl(Object input) { return String.valueOf(input); } + private String parseValueImpl(@NotNull Object input, @NotNull Locale locale) { + if (!(input instanceof String)) { + throw new CoercingParseValueException( + i18nMsg(locale, "String.unexpectedRawValueType", typeName(input)) + ); + } + return (String) input; + } + private String parseLiteralImpl(@NotNull Object input, Locale locale) { if (!(input instanceof StringValue)) { throw new CoercingParseLiteralException( @@ -55,12 +64,12 @@ public String serialize(@NotNull Object dataFetcherResult) { @Override @Deprecated public String parseValue(@NotNull Object input) { - return toStringImpl(input); + return parseValueImpl(input, Locale.getDefault()); } @Override public String parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { - return toStringImpl(input); + return parseValueImpl(input, locale); } @Override @@ -76,7 +85,7 @@ public String parseLiteral(@NotNull Object input) { @Override @Deprecated - public Value valueToLiteral(@NotNull Object input) { + public @NotNull Value valueToLiteral(@NotNull Object input) { return valueToLiteralImpl(input); } diff --git a/src/test/groovy/graphql/ScalarsStringTest.groovy b/src/test/groovy/graphql/ScalarsStringTest.groovy index 536dd07216..cbd00f97d9 100644 --- a/src/test/groovy/graphql/ScalarsStringTest.groovy +++ b/src/test/groovy/graphql/ScalarsStringTest.groovy @@ -4,6 +4,7 @@ import graphql.execution.CoercedVariables import graphql.language.BooleanValue import graphql.language.StringValue import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingParseValueException import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll @@ -85,15 +86,24 @@ class ScalarsStringTest extends Specification { } @Unroll - def "String parseValue can parse non-String values"() { - expect: - Scalars.GraphQLString.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) == result + def "String parseValue throws exception for non-String values"() { + when: + Scalars.GraphQLString.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) + then: + thrown(CoercingParseValueException) where: - value | result - 123 | "123" - true | "true" - customObject | "foo" + value | _ + 123 | _ + true | _ + customObject | _ } + def "String parseValue English exception message"() { + when: + Scalars.GraphQLString.getCoercing().parseValue(9001, GraphQLContext.default, Locale.ENGLISH) + then: + def ex = thrown(CoercingParseValueException) + ex.message == "Expected a String input, but it was a 'Integer'" + } } From 0336f9bb74c36c446671cc4931e167d8c5356597 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:39:41 +1100 Subject: [PATCH 372/393] Add stricter Boolean parseValue coercion --- .../scalar/GraphqlBooleanCoercing.java | 7 ++-- .../groovy/graphql/ScalarsBooleanTest.groovy | 32 +++++++------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java index ceddc7558c..c5a26b571b 100644 --- a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java @@ -67,13 +67,12 @@ private Boolean serializeImpl(@NotNull Object input, @NotNull Locale locale) { @NotNull private Boolean parseValueImpl(@NotNull Object input, @NotNull Locale locale) { - Boolean result = convertImpl(input); - if (result == null) { + if (!(input instanceof Boolean)) { throw new CoercingParseValueException( - i18nMsg(locale, "Boolean.notBoolean", typeName(input)) + i18nMsg(locale, "Boolean.unexpectedRawValueType", typeName(input)) ); } - return result; + return (Boolean) input; } private static boolean parseLiteralImpl(@NotNull Object input, @NotNull Locale locale) { diff --git a/src/test/groovy/graphql/ScalarsBooleanTest.groovy b/src/test/groovy/graphql/ScalarsBooleanTest.groovy index a045d1249d..5b316765ad 100644 --- a/src/test/groovy/graphql/ScalarsBooleanTest.groovy +++ b/src/test/groovy/graphql/ScalarsBooleanTest.groovy @@ -131,27 +131,6 @@ class ScalarsBooleanTest extends Specification { false | false } - @Unroll - def "parseValue parses non-Boolean input #value"() { - expect: - Scalars.GraphQLBoolean.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) == result - - where: - value | result - true | true - "false" | false - "true" | true - "True" | true - 0 | false - 1 | true - -1 | true - new Long(42345784398534785l) | true - new Double(42.3) | true - new Float(42.3) | true - Integer.MAX_VALUE + 1l | true - Integer.MIN_VALUE - 1l | true - } - @Unroll def "parseValue throws exception for invalid input #value"() { when: @@ -162,6 +141,17 @@ class ScalarsBooleanTest extends Specification { where: value | _ new Object() | _ + "false" | _ + "true" | _ + "True" | _ + 0 | _ + 1 | _ + -1 | _ + new Long(42345784398534785l) | _ + new Double(42.3) | _ + new Float(42.3) | _ + Integer.MAX_VALUE + 1l | _ + Integer.MIN_VALUE - 1l | _ } } From 2aecb85a468e4ff73540ec9749df1ccd120e4577 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:52:35 +1100 Subject: [PATCH 373/393] Add stricter float parseValue coercion --- .../scalar/GraphqlBooleanCoercing.java | 2 +- .../graphql/scalar/GraphqlFloatCoercing.java | 6 ++ .../groovy/graphql/ScalarsBooleanTest.groovy | 78 +++++++++---------- .../groovy/graphql/ScalarsFloatTest.groovy | 33 ++++---- 4 files changed, 61 insertions(+), 58 deletions(-) diff --git a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java index c5a26b571b..8c06b874e6 100644 --- a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java @@ -128,7 +128,7 @@ public Boolean parseLiteral(@NotNull Object input) { @Override @Deprecated - public Value valueToLiteral(@NotNull Object input) { + public @NotNull Value valueToLiteral(@NotNull Object input) { return valueToLiteralImpl(input, Locale.getDefault()); } diff --git a/src/main/java/graphql/scalar/GraphqlFloatCoercing.java b/src/main/java/graphql/scalar/GraphqlFloatCoercing.java index e5e3526e8a..7efa270274 100644 --- a/src/main/java/graphql/scalar/GraphqlFloatCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlFloatCoercing.java @@ -65,6 +65,12 @@ private Double serialiseImpl(Object input, @NotNull Locale locale) { @NotNull private Double parseValueImpl(@NotNull Object input, @NotNull Locale locale) { + if (!(input instanceof Number)) { + throw new CoercingParseValueException( + i18nMsg(locale, "Float.unexpectedRawValueType", typeName(input)) + ); + } + Double result = convertImpl(input); if (result == null) { throw new CoercingParseValueException( diff --git a/src/test/groovy/graphql/ScalarsBooleanTest.groovy b/src/test/groovy/graphql/ScalarsBooleanTest.groovy index 5b316765ad..55351f7f2b 100644 --- a/src/test/groovy/graphql/ScalarsBooleanTest.groovy +++ b/src/test/groovy/graphql/ScalarsBooleanTest.groovy @@ -55,19 +55,19 @@ class ScalarsBooleanTest extends Specification { Scalars.GraphQLBoolean.getCoercing().serialize(value, GraphQLContext.default, Locale.default) == result where: - value | result - true | true - "false" | false - "true" | true - "True" | true - 0 | false - 1 | true - -1 | true - new Long(42345784398534785l) | true - new Double(42.3) | true - new Float(42.3) | true - Integer.MAX_VALUE + 1l | true - Integer.MIN_VALUE - 1l | true + value | result + true | true + "false" | false + "true" | true + "True" | true + 0 | false + 1 | true + -1 | true + Long.valueOf(42345784398534785l) | true + Double.valueOf(42.3) | true + Float.valueOf(42.3) | true + Integer.MAX_VALUE + 1l | true + Integer.MIN_VALUE - 1l | true } @Unroll @@ -76,19 +76,19 @@ class ScalarsBooleanTest extends Specification { Scalars.GraphQLBoolean.getCoercing().serialize(value) == result // Retain deprecated method for test coverage where: - value | result - true | true - "false" | false - "true" | true - "True" | true - 0 | false - 1 | true - -1 | true - new Long(42345784398534785l) | true - new Double(42.3) | true - new Float(42.3) | true - Integer.MAX_VALUE + 1l | true - Integer.MIN_VALUE - 1l | true + value | result + true | true + "false" | false + "true" | true + "True" | true + 0 | false + 1 | true + -1 | true + Long.valueOf(42345784398534785l) | true + Double.valueOf(42.3) | true + Float.valueOf(42.3) | true + Integer.MAX_VALUE + 1l | true + Integer.MIN_VALUE - 1l | true } @Unroll @@ -139,19 +139,19 @@ class ScalarsBooleanTest extends Specification { thrown(CoercingParseValueException) where: - value | _ - new Object() | _ - "false" | _ - "true" | _ - "True" | _ - 0 | _ - 1 | _ - -1 | _ - new Long(42345784398534785l) | _ - new Double(42.3) | _ - new Float(42.3) | _ - Integer.MAX_VALUE + 1l | _ - Integer.MIN_VALUE - 1l | _ + value | _ + new Object() | _ + "false" | _ + "true" | _ + "True" | _ + 0 | _ + 1 | _ + -1 | _ + Long.valueOf(42345784398534785l) | _ + Double.valueOf(42.3) | _ + Float.valueOf(42.3) | _ + Integer.MAX_VALUE + 1l | _ + Integer.MIN_VALUE - 1l | _ } } diff --git a/src/test/groovy/graphql/ScalarsFloatTest.groovy b/src/test/groovy/graphql/ScalarsFloatTest.groovy index 18846f788e..744fcbac03 100644 --- a/src/test/groovy/graphql/ScalarsFloatTest.groovy +++ b/src/test/groovy/graphql/ScalarsFloatTest.groovy @@ -64,15 +64,15 @@ class ScalarsFloatTest extends Specification { "42" | 42d "42.123" | 42.123d 42.0000d | 42 - new Integer(42) | 42 + Integer.valueOf(42) | 42 "-1" | -1 new BigInteger("42") | 42 new BigDecimal("42") | 42 new BigDecimal("4.2") | 4.2d 42.3f | 42.3d 42.0d | 42d - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567d new AtomicInteger(42) | 42 Double.MAX_VALUE | Double.MAX_VALUE @@ -89,15 +89,15 @@ class ScalarsFloatTest extends Specification { "42" | 42d "42.123" | 42.123d 42.0000d | 42 - new Integer(42) | 42 + Integer.valueOf(42) | 42 "-1" | -1 new BigInteger("42") | 42 new BigDecimal("42") | 42 new BigDecimal("4.2") | 4.2d 42.3f | 42.3d 42.0d | 42d - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567d new AtomicInteger(42) | 42 Double.MAX_VALUE | Double.MAX_VALUE @@ -137,21 +137,18 @@ class ScalarsFloatTest extends Specification { where: value | result 42.0000d | 42 - new Integer(42) | 42 + Integer.valueOf(42) | 42 new BigInteger("42") | 42 new BigDecimal("42") | 42 new BigDecimal("4.2") | 4.2d 42.3f | 42.3d 42.0d | 42d - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567d new AtomicInteger(42) | 42 Double.MAX_VALUE | Double.MAX_VALUE Double.MIN_VALUE | Double.MIN_VALUE - "42" | 42d - "42.123" | 42.123d - "-1" | -1 } @Unroll @@ -162,21 +159,18 @@ class ScalarsFloatTest extends Specification { where: value | result 42.0000d | 42 - new Integer(42) | 42 + Integer.valueOf(42) | 42 new BigInteger("42") | 42 new BigDecimal("42") | 42 new BigDecimal("4.2") | 4.2d 42.3f | 42.3d 42.0d | 42d - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567d new AtomicInteger(42) | 42 Double.MAX_VALUE | Double.MAX_VALUE Double.MIN_VALUE | Double.MIN_VALUE - "42" | 42d - "42.123" | 42.123d - "-1" | -1 } @@ -203,6 +197,9 @@ class ScalarsFloatTest extends Specification { Float.POSITIVE_INFINITY.toString() | _ Float.NEGATIVE_INFINITY | _ Float.NEGATIVE_INFINITY.toString() | _ + "42" | _ + "42.123" | _ + "-1" | _ } } From ea7661f15dd7e138f3c976327f9c5c501469699a Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:01:59 +1100 Subject: [PATCH 374/393] Add back stricter parseValue coercion for Int --- .../graphql/scalar/GraphqlIntCoercing.java | 35 ++++++++- src/test/groovy/graphql/ScalarsIntTest.groovy | 73 +++++++++---------- 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/src/main/java/graphql/scalar/GraphqlIntCoercing.java b/src/main/java/graphql/scalar/GraphqlIntCoercing.java index 0a4a055883..cf428fc8cf 100644 --- a/src/main/java/graphql/scalar/GraphqlIntCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlIntCoercing.java @@ -64,15 +64,44 @@ private Integer serialiseImpl(Object input, @NotNull Locale locale) { @NotNull private Integer parseValueImpl(@NotNull Object input, @NotNull Locale locale) { - Integer result = convertImpl(input); + if (!(input instanceof Number)) { + throw new CoercingParseValueException( + i18nMsg(locale, "Int.notInt", typeName(input)) + ); + } + if (input instanceof Integer) { + return (Integer) input; + } + + BigInteger result = convertParseValueImpl(input); if (result == null) { throw new CoercingParseValueException( i18nMsg(locale, "Int.notInt", typeName(input)) ); } + if (result.compareTo(INT_MIN) < 0 || result.compareTo(INT_MAX) > 0) { + throw new CoercingParseValueException( + i18nMsg(locale, "Int.outsideRange", result.toString()) + ); + } + return result.intValueExact(); + } - return result; + private BigInteger convertParseValueImpl(Object input) { + BigDecimal value; + try { + value = new BigDecimal(input.toString()); + } catch (NumberFormatException e) { + return null; + } + + try { + return value.toBigIntegerExact(); + } catch (ArithmeticException e) { + // Exception if number has non-zero fractional part + return null; + } } private static int parseLiteralImpl(Object input, @NotNull Locale locale) { @@ -134,7 +163,7 @@ public Integer parseLiteral(@NotNull Object input) { @Override @Deprecated - public Value valueToLiteral(@NotNull Object input) { + public @NotNull Value valueToLiteral(@NotNull Object input) { return valueToLiteralImpl(input, Locale.getDefault()); } diff --git a/src/test/groovy/graphql/ScalarsIntTest.groovy b/src/test/groovy/graphql/ScalarsIntTest.groovy index a38de1a49f..42c7c6887f 100644 --- a/src/test/groovy/graphql/ScalarsIntTest.groovy +++ b/src/test/groovy/graphql/ScalarsIntTest.groovy @@ -66,14 +66,14 @@ class ScalarsIntTest extends Specification { "42" | 42 "42.0000" | 42 42.0000d | 42 - new Integer(42) | 42 + Integer.valueOf(42) | 42 "-1" | -1 new BigInteger("42") | 42 new BigDecimal("42") | 42 42.0f | 42 42.0d | 42 - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567 new AtomicInteger(42) | 42 Integer.MAX_VALUE | Integer.MAX_VALUE @@ -90,14 +90,14 @@ class ScalarsIntTest extends Specification { "42" | 42 "42.0000" | 42 42.0000d | 42 - new Integer(42) | 42 + Integer.valueOf(42) | 42 "-1" | -1 new BigInteger("42") | 42 new BigDecimal("42") | 42 42.0f | 42 42.0d | 42 - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567 new AtomicInteger(42) | 42 Integer.MAX_VALUE | Integer.MAX_VALUE @@ -112,16 +112,16 @@ class ScalarsIntTest extends Specification { thrown(CoercingSerializeException) where: - value | _ - "" | _ - "not a number " | _ - "42.3" | _ - new Long(42345784398534785l) | _ - new Double(42.3) | _ - new Float(42.3) | _ - Integer.MAX_VALUE + 1l | _ - Integer.MIN_VALUE - 1l | _ - new Object() | _ + value | _ + "" | _ + "not a number " | _ + "42.3" | _ + Long.valueOf(42345784398534785l) | _ + Double.valueOf(42.3) | _ + Float.valueOf(42.3) | _ + Integer.MAX_VALUE + 1l | _ + Integer.MIN_VALUE - 1l | _ + new Object() | _ } @Unroll @@ -131,10 +131,10 @@ class ScalarsIntTest extends Specification { where: value | result - new Integer(42) | 42 + Integer.valueOf(42) | 42 new BigInteger("42") | 42 - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567 new AtomicInteger(42) | 42 42.0000d | 42 @@ -143,9 +143,6 @@ class ScalarsIntTest extends Specification { 42.0d | 42 Integer.MAX_VALUE | Integer.MAX_VALUE Integer.MIN_VALUE | Integer.MIN_VALUE - "42" | 42 - "42.0000" | 42 - "-1" | -1 } @Unroll @@ -155,10 +152,10 @@ class ScalarsIntTest extends Specification { where: value | result - new Integer(42) | 42 + Integer.valueOf(42) | 42 new BigInteger("42") | 42 - new Byte("42") | 42 - new Short("42") | 42 + Byte.valueOf("42") | 42 + Short.valueOf("42") | 42 1234567l | 1234567 new AtomicInteger(42) | 42 42.0000d | 42 @@ -167,9 +164,6 @@ class ScalarsIntTest extends Specification { 42.0d | 42 Integer.MAX_VALUE | Integer.MAX_VALUE Integer.MIN_VALUE | Integer.MIN_VALUE - "42" | 42 - "42.0000" | 42 - "-1" | -1 } @Unroll @@ -180,16 +174,19 @@ class ScalarsIntTest extends Specification { thrown(CoercingParseValueException) where: - value | _ - "" | _ - "not a number " | _ - "42.3" | _ - new Long(42345784398534785l) | _ - new Double(42.3) | _ - new Float(42.3) | _ - Integer.MAX_VALUE + 1l | _ - Integer.MIN_VALUE - 1l | _ - new Object() | _ + value | _ + "" | _ + "not a number " | _ + "42.3" | _ + Long.valueOf(42345784398534785l) | _ + Double.valueOf(42.3) | _ + Float.valueOf(42.3) | _ + Integer.MAX_VALUE + 1l | _ + Integer.MIN_VALUE - 1l | _ + new Object() | _ + "42" | _ + "42.0000" | _ + "-1" | _ } } From 8dfdcc419a258c26a2e673f3b202274e0e4666d7 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:02:07 +1100 Subject: [PATCH 375/393] Fix tests --- src/test/groovy/graphql/execution/ValuesResolverTest.groovy | 4 ++-- .../graphql/execution/values/InputInterceptorTest.groovy | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 83488658d2..1a7aa22b3e 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -1272,7 +1272,7 @@ class ValuesResolverTest extends Specification { executionResult.data == null executionResult.errors.size() == 1 executionResult.errors[0].errorType == ErrorType.ValidationError - executionResult.errors[0].message == "Variable 'input' has an invalid value: Expected a value that can be converted to type 'Boolean' but it was a 'String'" + executionResult.errors[0].message == "Variable 'input' has an invalid value: Expected a Boolean input, but it was a 'String'" executionResult.errors[0].locations == [new SourceLocation(2, 35)] } @@ -1310,7 +1310,7 @@ class ValuesResolverTest extends Specification { executionResult.data == null executionResult.errors.size() == 1 executionResult.errors[0].errorType == ErrorType.ValidationError - executionResult.errors[0].message == "Variable 'input' has an invalid value: Expected a value that can be converted to type 'Float' but it was a 'String'" + executionResult.errors[0].message == "Variable 'input' has an invalid value: Expected a Number input, but it was a 'String'" executionResult.errors[0].locations == [new SourceLocation(2, 35)] } } diff --git a/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy b/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy index 037dd27486..de6914a50d 100644 --- a/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy +++ b/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy @@ -134,6 +134,6 @@ class InputInterceptorTest extends Specification { then: !er.errors.isEmpty() - er.errors[0].message == "Variable 'booleanArg' has an invalid value: Expected a value that can be converted to type 'Boolean' but it was a 'LinkedHashMap'" + er.errors[0].message == "Variable 'booleanArg' has an invalid value: Expected a Boolean input, but it was a 'LinkedHashMap'" } } From c52cd8a0f30012b762c8f598bdfed9be1d93c415 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:32:40 +0000 Subject: [PATCH 376/393] Bump net.bytebuddy:byte-buddy from 1.14.12 to 1.14.13 Bumps [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) from 1.14.12 to 1.14.13. - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.12...byte-buddy-1.14.13) --- updated-dependencies: - dependency-name: net.bytebuddy:byte-buddy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- agent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/build.gradle b/agent/build.gradle index 754e8af406..1cc23ecb3c 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -6,7 +6,7 @@ plugins { } dependencies { - implementation("net.bytebuddy:byte-buddy:1.14.12") + implementation("net.bytebuddy:byte-buddy:1.14.13") // graphql-java itself implementation(rootProject) } From 165674ac7e45f2ce652b81648837d4bdb4b9af4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:32:45 +0000 Subject: [PATCH 377/393] Bump net.bytebuddy:byte-buddy-agent from 1.14.12 to 1.14.13 Bumps [net.bytebuddy:byte-buddy-agent](https://github.com/raphw/byte-buddy) from 1.14.12 to 1.14.13. - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.14.12...byte-buddy-1.14.13) --- updated-dependencies: - dependency-name: net.bytebuddy:byte-buddy-agent dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- agent-test/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent-test/build.gradle b/agent-test/build.gradle index 288fd50045..280aed1bac 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -4,7 +4,7 @@ plugins { dependencies { implementation(rootProject) - implementation("net.bytebuddy:byte-buddy-agent:1.14.12") + implementation("net.bytebuddy:byte-buddy-agent:1.14.13") testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' From b7dc51664efa2cf90a80676cefbfc874fce212e7 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:34:20 +1100 Subject: [PATCH 378/393] Tidy up --- src/main/resources/i18n/Scalars.properties | 2 +- src/main/resources/i18n/Scalars_de.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/i18n/Scalars.properties b/src/main/resources/i18n/Scalars.properties index 85aa20b50f..39fc6b4105 100644 --- a/src/main/resources/i18n/Scalars.properties +++ b/src/main/resources/i18n/Scalars.properties @@ -30,4 +30,4 @@ Boolean.notBoolean=Expected a value that can be converted to type ''Boolean'' bu Boolean.unexpectedAstType=Expected an AST type of ''BooleanValue'' but it was a ''{0}'' Boolean.unexpectedRawValueType=Expected a Boolean input, but it was a ''{0}'' # -String.unexpectedRawValueType=Expected a String input, but it was a ''{0}'' \ No newline at end of file +String.unexpectedRawValueType=Expected a String input, but it was a ''{0}'' diff --git a/src/main/resources/i18n/Scalars_de.properties b/src/main/resources/i18n/Scalars_de.properties index 8c82638362..242046369b 100644 --- a/src/main/resources/i18n/Scalars_de.properties +++ b/src/main/resources/i18n/Scalars_de.properties @@ -30,4 +30,4 @@ Boolean.notBoolean=Erwartet wurde ein Wert, der in den Typ ''Boolean'' konvertie Boolean.unexpectedAstType=Erwartet wurde ein AST type ''BooleanValue'', aber es war ein ''{0}'' Boolean.unexpectedRawValueType=Erwartet wurde eine Boolean-Eingabe, aber es war ein ''{0}'' # -String.unexpectedRawValueType=Erwartet wurde eine String-Eingabe, aber es war ein ''{0}'' \ No newline at end of file +String.unexpectedRawValueType=Erwartet wurde eine String-Eingabe, aber es war ein ''{0}'' From d85046e9463618b513dc1f33a03cf39530d945e2 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Tue, 2 Apr 2024 05:19:11 -0700 Subject: [PATCH 379/393] feedback --- .../java/graphql/schema/GraphQLDirective.java | 10 +++--- src/test/groovy/graphql/TestUtil.groovy | 32 ++++++++++++++++--- .../graphql/schema/GraphQLArgumentTest.groovy | 20 ++++++------ .../GraphQLEnumValueDefinitionTest.groovy | 6 ++-- .../schema/GraphQLFieldDefinitionTest.groovy | 8 ++--- .../schema/GraphQLInputObjectFieldTest.groovy | 8 ++--- .../schema/GraphQLScalarTypeTest.groovy | 8 ++--- .../graphql/schema/SchemaTraverserTest.groovy | 22 ++++++------- .../schema/idl/SchemaGeneratorTest.groovy | 2 -- 9 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/main/java/graphql/schema/GraphQLDirective.java b/src/main/java/graphql/schema/GraphQLDirective.java index d0033804c9..ae54d05977 100644 --- a/src/main/java/graphql/schema/GraphQLDirective.java +++ b/src/main/java/graphql/schema/GraphQLDirective.java @@ -7,15 +7,13 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; -import java.util.Collections; -import java.util.EnumSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Consumer; import java.util.function.UnaryOperator; -import static graphql.Assert.*; +import static graphql.Assert.assertNotEmpty; +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertValidName; import static graphql.introspection.Introspection.DirectiveLocation; import static graphql.util.FpKit.getByName; diff --git a/src/test/groovy/graphql/TestUtil.groovy b/src/test/groovy/graphql/TestUtil.groovy index fb677bd21b..490e7cee93 100644 --- a/src/test/groovy/graphql/TestUtil.groovy +++ b/src/test/groovy/graphql/TestUtil.groovy @@ -3,10 +3,32 @@ package graphql import graphql.execution.MergedField import graphql.execution.MergedSelectionSet import graphql.introspection.Introspection.DirectiveLocation -import graphql.language.* +import graphql.language.Document +import graphql.language.Field +import graphql.language.NullValue +import graphql.language.ObjectTypeDefinition +import graphql.language.OperationDefinition +import graphql.language.ScalarTypeDefinition +import graphql.language.Type import graphql.parser.Parser -import graphql.schema.* -import graphql.schema.idl.* +import graphql.schema.Coercing +import graphql.schema.DataFetcher +import graphql.schema.GraphQLAppliedDirective +import graphql.schema.GraphQLAppliedDirectiveArgument +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLDirective +import graphql.schema.GraphQLInputType +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import graphql.schema.GraphQLType +import graphql.schema.TypeResolver +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.SchemaGenerator +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.TestMockedWiringFactory +import graphql.schema.idl.TypeRuntimeWiring +import graphql.schema.idl.WiringFactory import graphql.schema.idl.errors.SchemaProblem import groovy.json.JsonOutput @@ -176,13 +198,13 @@ class TestUtil { .replaceDirectives( definition.getDirectives() .stream() - .map({ mockDirective(it.getName(), DirectiveLocation.SCALAR) }) + .map({ mkDirective(it.getName(), DirectiveLocation.SCALAR) }) .collect(Collectors.toList())) .definition(definition) .build() } - static GraphQLDirective mockDirective(String name, DirectiveLocation location, GraphQLArgument arg = null) { + static GraphQLDirective mkDirective(String name, DirectiveLocation location, GraphQLArgument arg = null) { def b = newDirective().name(name).description(name).validLocation(location) if (arg != null) b.argument(arg) b.build() diff --git a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy index 5b977eb215..aa557e63b5 100644 --- a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy @@ -13,7 +13,7 @@ import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition import static graphql.schema.GraphQLObjectType.newObject import static graphql.schema.GraphQLSchema.newSchema -import static graphql.TestUtil.mockDirective +import static graphql.TestUtil.mkDirective class GraphQLArgumentTest extends Specification { @@ -23,7 +23,7 @@ class GraphQLArgumentTest extends Specification { .description("A1_description") .type(GraphQLInt) .deprecate("custom reason") - .withDirective(mockDirective("directive1", ARGUMENT_DEFINITION)) + .withDirective(mkDirective("directive1", ARGUMENT_DEFINITION)) .build() when: def transformedArgument = startingArgument.transform({ @@ -31,7 +31,7 @@ class GraphQLArgumentTest extends Specification { .name("A2") .description("A2_description") .type(GraphQLString) - .withDirective(mockDirective("directive3", ARGUMENT_DEFINITION)) + .withDirective(mkDirective("directive3", ARGUMENT_DEFINITION)) .value("VALUE") // Retain deprecated for test coverage .deprecate(null) .defaultValue("DEFAULT") // Retain deprecated for test coverage @@ -82,7 +82,7 @@ class GraphQLArgumentTest extends Specification { given: def builder = newArgument().name("A1") .type(GraphQLInt) - .withDirective(mockDirective("directive1", ARGUMENT_DEFINITION)) + .withDirective(mkDirective("directive1", ARGUMENT_DEFINITION)) when: argument = builder.build() @@ -97,8 +97,8 @@ class GraphQLArgumentTest extends Specification { when: argument = builder .clearDirectives() - .withDirective(mockDirective("directive2", ARGUMENT_DEFINITION)) - .withDirective(mockDirective("directive3", ARGUMENT_DEFINITION)) + .withDirective(mkDirective("directive2", ARGUMENT_DEFINITION)) + .withDirective(mkDirective("directive3", ARGUMENT_DEFINITION)) .build() then: @@ -110,9 +110,9 @@ class GraphQLArgumentTest extends Specification { when: argument = builder .replaceDirectives([ - mockDirective("directive1", ARGUMENT_DEFINITION), - mockDirective("directive2", ARGUMENT_DEFINITION), - mockDirective("directive3", ARGUMENT_DEFINITION)]) // overwrite + mkDirective("directive1", ARGUMENT_DEFINITION), + mkDirective("directive2", ARGUMENT_DEFINITION), + mkDirective("directive3", ARGUMENT_DEFINITION)]) // overwrite .build() then: @@ -199,7 +199,7 @@ class GraphQLArgumentTest extends Specification { def "Applied schema directives arguments are validated for programmatic schemas"() { given: def arg = newArgument().name("arg").type(GraphQLInt).valueProgrammatic(ImmutableKit.emptyMap()).build() // Retain for test coverage - def directive = mockDirective("cached", ARGUMENT_DEFINITION, arg) + def directive = mkDirective("cached", ARGUMENT_DEFINITION, arg) def field = newFieldDefinition() .name("hello") .type(GraphQLString) diff --git a/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy b/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy index 216689d457..0a755a9f3c 100644 --- a/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLEnumValueDefinitionTest.groovy @@ -4,7 +4,7 @@ import static graphql.introspection.Introspection.DirectiveLocation import spock.lang.Specification import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition -import static graphql.TestUtil.mockDirective +import static graphql.TestUtil.mkDirective class GraphQLEnumValueDefinitionTest extends Specification { def "object can be transformed"() { @@ -12,14 +12,14 @@ class GraphQLEnumValueDefinitionTest extends Specification { def startEnumValue = newEnumValueDefinition().name("EV1") .description("EV1_description") .value("A") - .withDirective(mockDirective("directive1", DirectiveLocation.ENUM_VALUE)) + .withDirective(mkDirective("directive1", DirectiveLocation.ENUM_VALUE)) .build() when: def transformedEnumValue = startEnumValue.transform({ it .name("EV2") .value("X") - .withDirective(mockDirective("directive2", DirectiveLocation.ENUM_VALUE)) + .withDirective(mkDirective("directive2", DirectiveLocation.ENUM_VALUE)) }) then: diff --git a/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy b/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy index 8e578a4a96..46a5c3c1e5 100644 --- a/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy @@ -11,7 +11,7 @@ import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString import static graphql.TestUtil.mockArguments -import static graphql.TestUtil.mockDirective +import static graphql.TestUtil.mkDirective import static graphql.schema.DefaultGraphqlTypeComparatorRegistry.newComparators import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition @@ -36,8 +36,8 @@ class GraphQLFieldDefinitionTest extends Specification { .deprecate("F1_deprecated") .argument(newArgument().name("argStr").type(GraphQLString)) .argument(newArgument().name("argInt").type(GraphQLInt)) - .withDirective(mockDirective("directive1", Introspection.DirectiveLocation.FIELD_DEFINITION)) - .withDirective(mockDirective("directive2", Introspection.DirectiveLocation.FIELD_DEFINITION)) + .withDirective(mkDirective("directive1", Introspection.DirectiveLocation.FIELD_DEFINITION)) + .withDirective(mkDirective("directive2", Introspection.DirectiveLocation.FIELD_DEFINITION)) .build() when: @@ -48,7 +48,7 @@ class GraphQLFieldDefinitionTest extends Specification { .argument(newArgument().name("argStr").type(GraphQLString)) .argument(newArgument().name("argInt").type(GraphQLBoolean)) .argument(newArgument().name("argIntAdded").type(GraphQLInt)) - .withDirective(mockDirective("directive3", Introspection.DirectiveLocation.FIELD_DEFINITION)) + .withDirective(mkDirective("directive3", Introspection.DirectiveLocation.FIELD_DEFINITION)) }) then: diff --git a/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy b/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy index 62bf92f6df..67e3611442 100644 --- a/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLInputObjectFieldTest.groovy @@ -7,7 +7,7 @@ import spock.lang.Specification import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLInt import static graphql.schema.GraphQLInputObjectField.newInputObjectField -import static graphql.TestUtil.mockDirective +import static graphql.TestUtil.mkDirective class GraphQLInputObjectFieldTest extends Specification { @@ -17,8 +17,8 @@ class GraphQLInputObjectFieldTest extends Specification { .name("F1") .type(GraphQLFloat) .description("F1_description") - .withDirective(mockDirective("directive1", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) - .withDirective(mockDirective("directive2", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) + .withDirective(mkDirective("directive1", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) + .withDirective(mkDirective("directive2", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) .deprecate("No longer useful") .build() @@ -27,7 +27,7 @@ class GraphQLInputObjectFieldTest extends Specification { builder.name("F2") .type(GraphQLInt) .deprecate(null) - .withDirective(mockDirective("directive3", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) + .withDirective(mkDirective("directive3", Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) }) then: diff --git a/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy b/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy index b901ddc47c..100269838d 100644 --- a/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLScalarTypeTest.groovy @@ -3,7 +3,7 @@ package graphql.schema import graphql.introspection.Introspection import spock.lang.Specification -import static graphql.TestUtil.mockDirective +import static graphql.TestUtil.mkDirective class GraphQLScalarTypeTest extends Specification { Coercing coercing = new Coercing() { @@ -29,14 +29,14 @@ class GraphQLScalarTypeTest extends Specification { .name("S1") .description("S1_description") .coercing(coercing) - .withDirective(mockDirective("directive1", Introspection.DirectiveLocation.SCALAR)) - .withDirective(mockDirective("directive2", Introspection.DirectiveLocation.SCALAR)) + .withDirective(mkDirective("directive1", Introspection.DirectiveLocation.SCALAR)) + .withDirective(mkDirective("directive2", Introspection.DirectiveLocation.SCALAR)) .build() when: def transformedScalar = startingScalar.transform({ builder -> builder.name("S2") .description("S2_description") - .withDirective(mockDirective("directive3", Introspection.DirectiveLocation.SCALAR)) + .withDirective(mkDirective("directive3", Introspection.DirectiveLocation.SCALAR)) }) then: diff --git a/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy b/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy index ce2a523e21..f7a6fc8a9c 100644 --- a/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy @@ -10,7 +10,7 @@ import static graphql.introspection.Introspection.DirectiveLocation import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLTypeReference.typeRef import static graphql.schema.GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY -import static graphql.TestUtil.mockDirective +import static graphql.TestUtil.mkDirective class SchemaTraverserTest extends Specification { @@ -202,7 +202,7 @@ class SchemaTraverserTest extends Specification { def scalarType = GraphQLScalarType.newScalar() .name("foo") .coercing(coercing) - .withDirective(mockDirective("bar", DirectiveLocation.SCALAR)) + .withDirective(mkDirective("bar", DirectiveLocation.SCALAR)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -218,7 +218,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def objectType = GraphQLObjectType.newObject() .name("foo") - .withDirective(mockDirective("bar", DirectiveLocation.OBJECT)) + .withDirective(mkDirective("bar", DirectiveLocation.OBJECT)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -235,7 +235,7 @@ class SchemaTraverserTest extends Specification { def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition() .name("foo") .type(Scalars.GraphQLString) - .withDirective(mockDirective("bar", DirectiveLocation.FIELD_DEFINITION)) + .withDirective(mkDirective("bar", DirectiveLocation.FIELD_DEFINITION)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -252,7 +252,7 @@ class SchemaTraverserTest extends Specification { def argument = newArgument() .name("foo") .type(Scalars.GraphQLString) - .withDirective(mockDirective("bar", DirectiveLocation.ARGUMENT_DEFINITION)) + .withDirective(mkDirective("bar", DirectiveLocation.ARGUMENT_DEFINITION)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -268,7 +268,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def interfaceType = GraphQLInterfaceType.newInterface() .name("foo") - .withDirective(mockDirective("bar", DirectiveLocation.INTERFACE)) + .withDirective(mkDirective("bar", DirectiveLocation.INTERFACE)) .withAppliedDirective(GraphQLAppliedDirective.newDirective() .name("barApplied")) .build() @@ -285,7 +285,7 @@ class SchemaTraverserTest extends Specification { def unionType = GraphQLUnionType.newUnionType() .name("foo") .possibleType(GraphQLObjectType.newObject().name("dummy").build()) - .withDirective(mockDirective("bar", DirectiveLocation.UNION)) + .withDirective(mkDirective("bar", DirectiveLocation.UNION)) .build() new SchemaTraverser().depthFirst(visitor, unionType) then: @@ -298,7 +298,7 @@ class SchemaTraverserTest extends Specification { def enumType = GraphQLEnumType.newEnum() .name("foo") .value("dummy") - .withDirective(mockDirective("bar", DirectiveLocation.ENUM)) + .withDirective(mkDirective("bar", DirectiveLocation.ENUM)) .build() new SchemaTraverser().depthFirst(visitor, enumType) then: @@ -310,7 +310,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def enumValue = GraphQLEnumValueDefinition.newEnumValueDefinition() .name("foo") - .withDirective(mockDirective("bar", DirectiveLocation.ENUM_VALUE)) + .withDirective(mkDirective("bar", DirectiveLocation.ENUM_VALUE)) .build() new SchemaTraverser().depthFirst(visitor, enumValue) then: @@ -322,7 +322,7 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def inputObjectType = GraphQLInputObjectType.newInputObject() .name("foo") - .withDirective(mockDirective("bar", DirectiveLocation.INPUT_OBJECT)) + .withDirective(mkDirective("bar", DirectiveLocation.INPUT_OBJECT)) .build() new SchemaTraverser().depthFirst(visitor, inputObjectType) then: @@ -335,7 +335,7 @@ class SchemaTraverserTest extends Specification { def inputField = GraphQLInputObjectField.newInputObjectField() .name("foo") .type(Scalars.GraphQLString) - .withDirective(mockDirective("bar", DirectiveLocation.INPUT_FIELD_DEFINITION)) + .withDirective(mkDirective("bar", DirectiveLocation.INPUT_FIELD_DEFINITION)) .build() new SchemaTraverser().depthFirst(visitor, inputField) then: diff --git a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy index ba572a7f8c..d8895370c6 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy @@ -8,7 +8,6 @@ import graphql.schema.DataFetcherFactory import graphql.schema.DataFetcherFactoryEnvironment import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLAppliedDirective -import graphql.schema.GraphQLArgument import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLDirectiveContainer import graphql.schema.GraphQLEnumType @@ -42,7 +41,6 @@ import static graphql.language.AstPrinter.printAst import static graphql.schema.GraphQLCodeRegistry.newCodeRegistry import static graphql.schema.idl.SchemaGenerator.Options.defaultOptions import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring -import static graphql.TestUtil.mockDirective class SchemaGeneratorTest extends Specification { From 479440ddf254fe2f7e410e9200ace1a7892edc47 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Tue, 2 Apr 2024 05:22:35 -0700 Subject: [PATCH 380/393] fix java wildcard imports --- src/main/java/graphql/schema/GraphQLDirective.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/schema/GraphQLDirective.java b/src/main/java/graphql/schema/GraphQLDirective.java index ae54d05977..9037482f21 100644 --- a/src/main/java/graphql/schema/GraphQLDirective.java +++ b/src/main/java/graphql/schema/GraphQLDirective.java @@ -7,7 +7,11 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; -import java.util.*; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.function.UnaryOperator; From e0fb7d0cd2e88db06b410828671b578203d452d1 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 4 Apr 2024 19:33:26 +1100 Subject: [PATCH 381/393] Putting createState back into Instrumentation --- .../instrumentation/Instrumentation.java | 15 ++++++- .../SimplePerformantInstrumentation.java | 6 +++ .../InstrumentationTest.groovy | 42 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index ef3f12e84e..15470ce735 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -50,9 +50,22 @@ public interface Instrumentation { */ @Nullable default CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { - return null; + return CompletableFuture.completedFuture(createState(parameters)); } + /** + * This method is retained for backwards compatibility reasons so that previous {@link Instrumentation} implementations + * continue to work. The graphql-java code only called {@link #createStateAsync(InstrumentationCreateStateParameters)} + * but the default implementation calls back to this method. + * + * @param parameters the parameters to this step + * + * @return a state object that is passed to each method + */ + @Nullable + default InstrumentationState createState(InstrumentationCreateStateParameters parameters) { + return null; + } /** * This is called right at the start of query execution, and it's the first step in the instrumentation chain. diff --git a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java index 61a15a1247..dfffa5b729 100644 --- a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java @@ -48,6 +48,12 @@ public class SimplePerformantInstrumentation implements Instrumentation { @Override public @Nullable CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { + InstrumentationState state = createState(parameters); + return state == null ? null : CompletableFuture.completedFuture(state); + } + + @Override + public @Nullable InstrumentationState createState(InstrumentationCreateStateParameters parameters) { return null; } diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index f3f385ae24..a92cf94518 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -456,4 +456,46 @@ class InstrumentationTest extends Specification { then: er.extensions == [i1: "I1"] } + + def "can have an backwards compatibility createState() in play"() { + + + given: + + def query = '''query Q($var: String!) { + human(id: $var) { + id + name + } + } + ''' + + + def instrumentation1 = new SimplePerformantInstrumentation() { + + @Override + InstrumentationState createState(InstrumentationCreateStateParameters parameters) { + return new StringInstrumentationState("I1") + } + + @Override + CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { + return CompletableFuture.completedFuture( + executionResult.transform { it.addExtension("i1", ((StringInstrumentationState) state).value) } + ) + } + } + + def graphQL = GraphQL + .newGraphQL(StarWarsSchema.starWarsSchema) + .instrumentation(instrumentation1) + .build() + + when: + def variables = [var: "1001"] + def er = graphQL.execute(ExecutionInput.newExecutionInput().query(query).variables(variables)) // Luke + + then: + er.extensions == [i1: "I1"] + } } From 93489b8e9fd1ea912cc944edaec25ad6b6dd2b77 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 4 Apr 2024 19:50:48 +1100 Subject: [PATCH 382/393] Putting createState back into Instrumentation - null option --- .../graphql/execution/instrumentation/Instrumentation.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 15470ce735..977422e565 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -50,7 +50,8 @@ public interface Instrumentation { */ @Nullable default CompletableFuture createStateAsync(InstrumentationCreateStateParameters parameters) { - return CompletableFuture.completedFuture(createState(parameters)); + InstrumentationState state = createState(parameters); + return state == null ? null : CompletableFuture.completedFuture(state); } /** From 1c8eed94743db8041044ffcdae2a08e4024553b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:37:55 +0000 Subject: [PATCH 383/393] Bump org.testng:testng from 7.9.0 to 7.10.0 Bumps [org.testng:testng](https://github.com/testng-team/testng) from 7.9.0 to 7.10.0. - [Release notes](https://github.com/testng-team/testng/releases) - [Changelog](https://github.com/testng-team/testng/blob/master/CHANGES.txt) - [Commits](https://github.com/testng-team/testng/compare/7.9.0...7.10.0) --- updated-dependencies: - dependency-name: org.testng:testng dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15d512e833..c457eb4bda 100644 --- a/build.gradle +++ b/build.gradle @@ -118,7 +118,7 @@ dependencies { testImplementation 'org.reactivestreams:reactive-streams-tck:' + reactiveStreamsVersion testImplementation "io.reactivex.rxjava2:rxjava:2.2.21" - testImplementation 'org.testng:testng:7.9.0' // use for reactive streams test inheritance + testImplementation 'org.testng:testng:7.10.0' // use for reactive streams test inheritance testImplementation 'org.openjdk.jmh:jmh-core:1.37' testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' From 81547a13259f8484663574be0334230577b6434f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:37:57 +0000 Subject: [PATCH 384/393] Bump io.github.gradle-nexus.publish-plugin from 1.3.0 to 2.0.0 Bumps io.github.gradle-nexus.publish-plugin from 1.3.0 to 2.0.0. --- updated-dependencies: - dependency-name: io.github.gradle-nexus.publish-plugin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15d512e833..b6bfeb0aeb 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ plugins { id 'signing' id "com.github.johnrengelman.shadow" version "8.1.1" id "biz.aQute.bnd.builder" version "6.4.0" - id "io.github.gradle-nexus.publish-plugin" version "1.3.0" + id "io.github.gradle-nexus.publish-plugin" version "2.0.0" id "groovy" id "me.champeau.jmh" version "0.7.2" } From 3b44101df6b3ac8cc347f088f9986eddc4c366b4 Mon Sep 17 00:00:00 2001 From: Steven Barker Date: Wed, 10 Apr 2024 11:52:31 +1200 Subject: [PATCH 385/393] ficx incremental partial result builder --- .../incremental/DelayedIncrementalPartialResultImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/incremental/DelayedIncrementalPartialResultImpl.java b/src/main/java/graphql/incremental/DelayedIncrementalPartialResultImpl.java index 4a34a56305..461d658e7d 100644 --- a/src/main/java/graphql/incremental/DelayedIncrementalPartialResultImpl.java +++ b/src/main/java/graphql/incremental/DelayedIncrementalPartialResultImpl.java @@ -75,8 +75,8 @@ public Builder incrementalItems(List incrementalItems) { return this; } - public Builder extensions(boolean hasNext) { - this.hasNext = hasNext; + public Builder extensions(Map extensions) { + this.extensions = extensions; return this; } From 6291c99ba8ebdcb8db87b0654d32084c6cac15da Mon Sep 17 00:00:00 2001 From: Steven Barker Date: Wed, 10 Apr 2024 16:03:55 +1200 Subject: [PATCH 386/393] add extensions to IncrementalExecutionResult sanity test --- .../graphql/incremental/IncrementalExecutionResultTest.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy index d30747fccc..f2427d4929 100644 --- a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy +++ b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy @@ -40,6 +40,7 @@ class IncrementalExecutionResultTest extends Specification { ]) .hasNext(true) .incremental([defer1, stream1, stream2]) + .extensions([some: "map"]) .build() def toSpec = result.toSpecification() @@ -47,6 +48,7 @@ class IncrementalExecutionResultTest extends Specification { then: toSpec == [ data : [person: [name: "Luke Skywalker", films: [[title: "A New Hope"]]]], + extensions: [some: "map"], hasNext : true, incremental: [ [path: ["person"], label: "homeWorldDefer", data: [homeWorld: "Tatooine"]], From cb2104af749e9dec1e77a1416d69b1eb6b68ff68 Mon Sep 17 00:00:00 2001 From: Steven Barker Date: Wed, 10 Apr 2024 16:58:40 +1200 Subject: [PATCH 387/393] add sanity test for defer result builder --- .../IncrementalExecutionResultTest.groovy | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy index f2427d4929..9285109e27 100644 --- a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy +++ b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy @@ -9,7 +9,7 @@ import static graphql.incremental.StreamPayload.newStreamedItem class IncrementalExecutionResultTest extends Specification { - def "sanity test to check builders work"() { + def "sanity test to check IncrementalExecutionResultImpl and item builders work"() { when: def defer1 = newDeferredItem() .label("homeWorldDefer") @@ -58,4 +58,28 @@ class IncrementalExecutionResultTest extends Specification { ] } + def "sanity test to check DelayedIncrementalPartialResult builder works"() { + when: + def deferredItem = newDeferredItem() + .label("homeWorld") + .path(ResultPath.parse("/person")) + .data([homeWorld: "Tatooine"]) + .build() + + def result = DelayedIncrementalPartialResultImpl.newIncrementalExecutionResult() + .incrementalItems([deferredItem]) + .hasNext(false) + .extensions([some: "map"]) + .build() + + def toSpec = result.toSpecification() + + then: + toSpec == [ + incremental: [[path: ["person"], label: "homeWorld", data: [homeWorld: "Tatooine"]]], + extensions: [some: "map"], + hasNext : false, + ] + + } } From 19ead874e0c0889c83694169ab72f984bfe2c3a8 Mon Sep 17 00:00:00 2001 From: Steven Barker Date: Wed, 10 Apr 2024 16:59:28 +1200 Subject: [PATCH 388/393] change test name --- .../graphql/incremental/IncrementalExecutionResultTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy index 9285109e27..25eb197ec5 100644 --- a/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy +++ b/src/test/groovy/graphql/incremental/IncrementalExecutionResultTest.groovy @@ -9,7 +9,7 @@ import static graphql.incremental.StreamPayload.newStreamedItem class IncrementalExecutionResultTest extends Specification { - def "sanity test to check IncrementalExecutionResultImpl and item builders work"() { + def "sanity test to check IncrementalExecutionResultImpl builder and item builders work"() { when: def defer1 = newDeferredItem() .label("homeWorldDefer") From b2dc5107d61f31d85f7ac8922e3ac38f88c1158a Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 15 Apr 2024 09:08:34 +1000 Subject: [PATCH 389/393] Dataloader 3.3.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c7f216a0a9..641ed53c1b 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,7 @@ jar { dependencies { compileOnly 'org.jetbrains:annotations:24.1.0' implementation 'org.antlr:antlr4-runtime:' + antlrVersion - api 'com.graphql-java:java-dataloader:3.2.2' + api 'com.graphql-java:java-dataloader:3.3.0' api 'org.reactivestreams:reactive-streams:' + reactiveStreamsVersion antlr 'org.antlr:antlr4:' + antlrVersion implementation 'com.google.guava:guava:' + guavaVersion From 5a53ec4d339c206ce1ea1ec1f5b62ce6d131af6d Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 15 Apr 2024 22:24:25 +1000 Subject: [PATCH 390/393] strictMode for RuntimeWiring and TypeRuntimeWiring --- .../graphql/schema/idl/RuntimeWiring.java | 28 ++++++- .../graphql/schema/idl/TypeRuntimeWiring.java | 47 ++++++++++++ .../idl/errors/StrictModeWiringException.java | 17 +++++ .../schema/idl/RuntimeWiringTest.groovy | 75 +++++++++++++++---- .../schema/idl/TypeRuntimeWiringTest.groovy | 74 ++++++++++++++++++ 5 files changed, 225 insertions(+), 16 deletions(-) create mode 100644 src/main/java/graphql/schema/idl/errors/StrictModeWiringException.java create mode 100644 src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy diff --git a/src/main/java/graphql/schema/idl/RuntimeWiring.java b/src/main/java/graphql/schema/idl/RuntimeWiring.java index 9edc34a246..0a95adce08 100644 --- a/src/main/java/graphql/schema/idl/RuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/RuntimeWiring.java @@ -7,10 +7,10 @@ import graphql.schema.GraphQLSchema; import graphql.schema.GraphqlTypeComparatorRegistry; import graphql.schema.TypeResolver; +import graphql.schema.idl.errors.StrictModeWiringException; import graphql.schema.visibility.GraphqlFieldVisibility; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -19,6 +19,7 @@ import static graphql.Assert.assertNotNull; import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; +import static java.lang.String.format; /** * A runtime wiring is a specification of data fetchers, type resolvers and custom scalars that are needed @@ -161,6 +162,7 @@ public static class Builder { private final Map registeredDirectiveWiring = new LinkedHashMap<>(); private final List directiveWiring = new ArrayList<>(); private WiringFactory wiringFactory = new NoopWiringFactory(); + private boolean strictMode = false; private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY; private GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry().build(); private GraphqlTypeComparatorRegistry comparatorRegistry = GraphqlTypeComparatorRegistry.AS_IS_REGISTRY; @@ -169,6 +171,16 @@ private Builder() { ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS.forEach(this::scalar); } + /** + * This puts the builder into strict mode, so if things get defined twice, for example, it will throw a {@link StrictModeWiringException}. + * + * @return this builder + */ + public Builder strictMode() { + this.strictMode = true; + return this; + } + /** * Adds a wiring factory into the runtime wiring * @@ -214,6 +226,9 @@ public Builder codeRegistry(GraphQLCodeRegistry.Builder codeRegistry) { * @return the runtime wiring builder */ public Builder scalar(GraphQLScalarType scalarType) { + if (strictMode && scalars.containsKey(scalarType.getName())) { + throw new StrictModeWiringException(format("The scalar %s is already defined", scalarType.getName())); + } scalars.put(scalarType.getName(), scalarType); return this; } @@ -264,17 +279,26 @@ public Builder type(String typeName, UnaryOperator bu public Builder type(TypeRuntimeWiring typeRuntimeWiring) { String typeName = typeRuntimeWiring.getTypeName(); Map typeDataFetchers = dataFetchers.computeIfAbsent(typeName, k -> new LinkedHashMap<>()); - typeRuntimeWiring.getFieldDataFetchers().forEach(typeDataFetchers::put); + if (strictMode && !typeDataFetchers.isEmpty()) { + throw new StrictModeWiringException(format("The type %s has already been defined", typeName)); + } + typeDataFetchers.putAll(typeRuntimeWiring.getFieldDataFetchers()); defaultDataFetchers.put(typeName, typeRuntimeWiring.getDefaultDataFetcher()); TypeResolver typeResolver = typeRuntimeWiring.getTypeResolver(); if (typeResolver != null) { + if (strictMode && this.typeResolvers.containsKey(typeName)) { + throw new StrictModeWiringException(format("The type %s already has a type resolver defined", typeName)); + } this.typeResolvers.put(typeName, typeResolver); } EnumValuesProvider enumValuesProvider = typeRuntimeWiring.getEnumValuesProvider(); if (enumValuesProvider != null) { + if (strictMode && this.enumValuesProviders.containsKey(typeName)) { + throw new StrictModeWiringException(format("The type %s already has a enum provider defined", typeName)); + } this.enumValuesProviders.put(typeName, enumValuesProvider); } return this; diff --git a/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java b/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java index 60bfe6f60c..1626139175 100644 --- a/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java @@ -4,12 +4,15 @@ import graphql.schema.DataFetcher; import graphql.schema.GraphQLSchema; import graphql.schema.TypeResolver; +import graphql.schema.idl.errors.StrictModeWiringException; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.UnaryOperator; import static graphql.Assert.assertNotNull; +import static java.lang.String.format; /** * A type runtime wiring is a specification of the data fetchers and possible type resolver for a given type name. @@ -18,6 +21,28 @@ */ @PublicApi public class TypeRuntimeWiring { + + private final static AtomicBoolean DEFAULT_STRICT_MODE = new AtomicBoolean(false); + + /** + * By default {@link TypeRuntimeWiring} builders are not in strict mode, but you can set a JVM wide value + * so that any created will be. + * + * @param strictMode the desired strict mode state + * + * @see Builder#strictMode() + */ + public static void setStrictModeJvmWide(boolean strictMode) { + DEFAULT_STRICT_MODE.set(strictMode); + } + + /** + * @return the current JVM wide state of strict mode + */ + public static boolean getStrictModeJvmWide() { + return DEFAULT_STRICT_MODE.get(); + } + private final String typeName; private final DataFetcher defaultDataFetcher; private final Map fieldDataFetchers; @@ -82,6 +107,7 @@ public static class Builder { private DataFetcher defaultDataFetcher; private TypeResolver typeResolver; private EnumValuesProvider enumValuesProvider; + private boolean strictMode = DEFAULT_STRICT_MODE.get(); /** * Sets the type name for this type wiring. You MUST set this. @@ -95,6 +121,17 @@ public Builder typeName(String typeName) { return this; } + /** + * This puts the builder into strict mode, so if things get defined twice, for example, it + * will throw a {@link StrictModeWiringException}. + * + * @return this builder + */ + public Builder strictMode() { + this.strictMode = true; + return this; + } + /** * Adds a data fetcher for the current type to the specified field * @@ -106,6 +143,9 @@ public Builder typeName(String typeName) { public Builder dataFetcher(String fieldName, DataFetcher dataFetcher) { assertNotNull(dataFetcher, () -> "you must provide a data fetcher"); assertNotNull(fieldName, () -> "you must tell us what field"); + if (strictMode && fieldDataFetchers.containsKey(fieldName)) { + throw new StrictModeWiringException(format("The field %s already has a data fetcher defined", fieldName)); + } fieldDataFetchers.put(fieldName, dataFetcher); return this; } @@ -119,6 +159,13 @@ public Builder dataFetcher(String fieldName, DataFetcher dataFetcher) { */ public Builder dataFetchers(Map dataFetchersMap) { assertNotNull(dataFetchersMap, () -> "you must provide a data fetchers map"); + if (strictMode) { + dataFetchersMap.forEach((fieldName, df) -> { + if (fieldDataFetchers.containsKey(fieldName)) { + throw new StrictModeWiringException(format("The field %s already has a data fetcher defined", fieldName)); + } + }); + } fieldDataFetchers.putAll(dataFetchersMap); return this; } diff --git a/src/main/java/graphql/schema/idl/errors/StrictModeWiringException.java b/src/main/java/graphql/schema/idl/errors/StrictModeWiringException.java new file mode 100644 index 0000000000..6da6b637fc --- /dev/null +++ b/src/main/java/graphql/schema/idl/errors/StrictModeWiringException.java @@ -0,0 +1,17 @@ +package graphql.schema.idl.errors; + +import graphql.GraphQLException; +import graphql.PublicApi; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.TypeRuntimeWiring; + +/** + * An exception that is throw when {@link RuntimeWiring.Builder#strictMode()} or {@link TypeRuntimeWiring.Builder#strictMode()} is true and + * something gets redefined. + */ +@PublicApi +public class StrictModeWiringException extends GraphQLException { + public StrictModeWiringException(String msg) { + super(msg); + } +} diff --git a/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy b/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy index a12b854fe7..bd3c225d2a 100644 --- a/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy +++ b/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy @@ -1,5 +1,6 @@ package graphql.schema.idl +import graphql.Scalars import graphql.TypeResolutionEnvironment import graphql.schema.Coercing import graphql.schema.DataFetcher @@ -9,6 +10,7 @@ import graphql.schema.GraphQLFieldsContainer import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.TypeResolver +import graphql.schema.idl.errors.StrictModeWiringException import graphql.schema.visibility.GraphqlFieldVisibility import spock.lang.Specification @@ -62,22 +64,22 @@ class RuntimeWiringTest extends Specification { def "basic call structure"() { def wiring = RuntimeWiring.newRuntimeWiring() .type("Query", { type -> - type - .dataFetcher("fieldX", new NamedDF("fieldX")) - .dataFetcher("fieldY", new NamedDF("fieldY")) - .dataFetcher("fieldZ", new NamedDF("fieldZ")) - .defaultDataFetcher(new NamedDF("defaultQueryDF")) - .typeResolver(new NamedTR("typeResolver4Query")) - } as UnaryOperator) + type + .dataFetcher("fieldX", new NamedDF("fieldX")) + .dataFetcher("fieldY", new NamedDF("fieldY")) + .dataFetcher("fieldZ", new NamedDF("fieldZ")) + .defaultDataFetcher(new NamedDF("defaultQueryDF")) + .typeResolver(new NamedTR("typeResolver4Query")) + } as UnaryOperator) .type("Mutation", { type -> - type - .dataFetcher("fieldX", new NamedDF("mfieldX")) - .dataFetcher("fieldY", new NamedDF("mfieldY")) - .dataFetcher("fieldZ", new NamedDF("mfieldZ")) - .defaultDataFetcher(new NamedDF("defaultMutationDF")) - .typeResolver(new NamedTR("typeResolver4Mutation")) - } as UnaryOperator) + type + .dataFetcher("fieldX", new NamedDF("mfieldX")) + .dataFetcher("fieldY", new NamedDF("mfieldY")) + .dataFetcher("fieldZ", new NamedDF("mfieldZ")) + .defaultDataFetcher(new NamedDF("defaultMutationDF")) + .typeResolver(new NamedTR("typeResolver4Mutation")) + } as UnaryOperator) .build() @@ -190,4 +192,49 @@ class RuntimeWiringTest extends Specification { newWiring.scalars["Custom2"] == customScalar2 newWiring.fieldVisibility == fieldVisibility } + + def "strict mode can stop certain redefinitions"() { + DataFetcher DF1 = env -> "x" + TypeResolver TR1 = env -> null + EnumValuesProvider EVP1 = name -> null + + when: + RuntimeWiring.newRuntimeWiring() + .strictMode() + .type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1)) + .type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF1)) + + + then: + def e1 = thrown(StrictModeWiringException) + e1.message == "The type Foo has already been defined" + + when: + RuntimeWiring.newRuntimeWiring() + .strictMode() + .type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1)) + .type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1)) + + then: + def e2 = thrown(StrictModeWiringException) + e2.message == "The type Foo already has a type resolver defined" + + when: + RuntimeWiring.newRuntimeWiring() + .strictMode() + .type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1)) + .type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1)) + then: + def e3 = thrown(StrictModeWiringException) + e3.message == "The type Foo already has a enum provider defined" + + when: + RuntimeWiring.newRuntimeWiring() + .strictMode() + .scalar(Scalars.GraphQLString) + then: + def e4 = thrown(StrictModeWiringException) + e4.message == "The scalar String is already defined" + + } } diff --git a/src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy b/src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy new file mode 100644 index 0000000000..49408b3d8f --- /dev/null +++ b/src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy @@ -0,0 +1,74 @@ +package graphql.schema.idl + +import graphql.schema.DataFetcher +import graphql.schema.idl.errors.StrictModeWiringException +import spock.lang.Specification + +class TypeRuntimeWiringTest extends Specification { + + void setup() { + TypeRuntimeWiring.setStrictModeJvmWide(false) + } + + void cleanup() { + TypeRuntimeWiring.setStrictModeJvmWide(false) + } + + DataFetcher DF1 = env -> "x" + DataFetcher DF2 = env -> "y" + + def "strict mode is off by default"() { + when: + def typeRuntimeWiring = TypeRuntimeWiring.newTypeWiring("Foo") + .dataFetcher("foo", DF1) + .dataFetcher("foo", DF2) + .build() + then: + typeRuntimeWiring.getFieldDataFetchers().get("foo") == DF2 + } + + def "strict mode can be turned on"() { + when: + TypeRuntimeWiring.newTypeWiring("Foo") + .strictMode() + .dataFetcher("foo", DF1) + .dataFetcher("foo", DF2) + .build() + then: + thrown(StrictModeWiringException) + } + + def "strict mode can be turned on JVM wide"() { + + + when: + def inStrictMode = TypeRuntimeWiring.getStrictModeJvmWide() + then: + !inStrictMode + + + when: + TypeRuntimeWiring.setStrictModeJvmWide(true) + inStrictMode = TypeRuntimeWiring.getStrictModeJvmWide() + + TypeRuntimeWiring.newTypeWiring("Foo") + .dataFetcher("foo", DF1) + .dataFetcher("foo", DF2) + .build() + then: + inStrictMode + thrown(StrictModeWiringException) + + when: + TypeRuntimeWiring.setStrictModeJvmWide(false) + inStrictMode = TypeRuntimeWiring.getStrictModeJvmWide() + + TypeRuntimeWiring.newTypeWiring("Foo") + .dataFetcher("foo", DF1) + .dataFetcher("foo", DF2) + .build() + then: + !inStrictMode + noExceptionThrown() + } +} From bb82efb811aec223448f312ef706b7c7856c35a2 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Mon, 15 Apr 2024 22:41:46 +1000 Subject: [PATCH 391/393] Better testing --- .../graphql/schema/idl/TypeRuntimeWiring.java | 14 +++++++++----- .../schema/idl/TypeRuntimeWiringTest.groovy | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java b/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java index 1626139175..3480a5d6b0 100644 --- a/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java @@ -143,8 +143,8 @@ public Builder strictMode() { public Builder dataFetcher(String fieldName, DataFetcher dataFetcher) { assertNotNull(dataFetcher, () -> "you must provide a data fetcher"); assertNotNull(fieldName, () -> "you must tell us what field"); - if (strictMode && fieldDataFetchers.containsKey(fieldName)) { - throw new StrictModeWiringException(format("The field %s already has a data fetcher defined", fieldName)); + if (strictMode) { + assertFieldStrictly(fieldName); } fieldDataFetchers.put(fieldName, dataFetcher); return this; @@ -161,15 +161,19 @@ public Builder dataFetchers(Map dataFetchersMap) { assertNotNull(dataFetchersMap, () -> "you must provide a data fetchers map"); if (strictMode) { dataFetchersMap.forEach((fieldName, df) -> { - if (fieldDataFetchers.containsKey(fieldName)) { - throw new StrictModeWiringException(format("The field %s already has a data fetcher defined", fieldName)); - } + assertFieldStrictly(fieldName); }); } fieldDataFetchers.putAll(dataFetchersMap); return this; } + private void assertFieldStrictly(String fieldName) { + if (fieldDataFetchers.containsKey(fieldName)) { + throw new StrictModeWiringException(format("The field %s already has a data fetcher defined", fieldName)); + } + } + /** * All fields in a type need a data fetcher of some sort and this method is called to provide the default data fetcher * that will be used for this type if no specific one has been provided per field. diff --git a/src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy b/src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy index 49408b3d8f..2a833ace22 100644 --- a/src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy +++ b/src/test/groovy/graphql/schema/idl/TypeRuntimeWiringTest.groovy @@ -35,7 +35,20 @@ class TypeRuntimeWiringTest extends Specification { .dataFetcher("foo", DF2) .build() then: - thrown(StrictModeWiringException) + def e = thrown(StrictModeWiringException) + e.message == "The field foo already has a data fetcher defined" + } + + def "strict mode can be turned on for maps of fields"() { + when: + TypeRuntimeWiring.newTypeWiring("Foo") + .strictMode() + .dataFetcher("foo", DF1) + .dataFetchers(["foo": DF2]) + .build() + then: + def e = thrown(StrictModeWiringException) + e.message == "The field foo already has a data fetcher defined" } def "strict mode can be turned on JVM wide"() { @@ -57,7 +70,8 @@ class TypeRuntimeWiringTest extends Specification { .build() then: inStrictMode - thrown(StrictModeWiringException) + def e = thrown(StrictModeWiringException) + e.message == "The field foo already has a data fetcher defined" when: TypeRuntimeWiring.setStrictModeJvmWide(false) From 9f613a6f3f45c3f659b55caab95678ab3a9b3331 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:44:43 +0000 Subject: [PATCH 392/393] Bump org.testng:testng from 7.10.0 to 7.10.1 Bumps [org.testng:testng](https://github.com/testng-team/testng) from 7.10.0 to 7.10.1. - [Release notes](https://github.com/testng-team/testng/releases) - [Changelog](https://github.com/testng-team/testng/blob/master/CHANGES.txt) - [Commits](https://github.com/testng-team/testng/compare/7.10.0...7.10.1) --- updated-dependencies: - dependency-name: org.testng:testng dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 641ed53c1b..3750da10cb 100644 --- a/build.gradle +++ b/build.gradle @@ -118,7 +118,7 @@ dependencies { testImplementation 'org.reactivestreams:reactive-streams-tck:' + reactiveStreamsVersion testImplementation "io.reactivex.rxjava2:rxjava:2.2.21" - testImplementation 'org.testng:testng:7.10.0' // use for reactive streams test inheritance + testImplementation 'org.testng:testng:7.10.1' // use for reactive streams test inheritance testImplementation 'org.openjdk.jmh:jmh-core:1.37' testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' From 207bb83b543c8134633d800450eabbc6a0edf391 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:46:28 +0000 Subject: [PATCH 393/393] Bump gradle/wrapper-validation-action from 2 to 3 Bumps [gradle/wrapper-validation-action](https://github.com/gradle/wrapper-validation-action) from 2 to 3. - [Release notes](https://github.com/gradle/wrapper-validation-action/releases) - [Commits](https://github.com/gradle/wrapper-validation-action/compare/v2...v3) --- updated-dependencies: - dependency-name: gradle/wrapper-validation-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/master.yml | 2 +- .github/workflows/pull_request.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 874fc59329..e0a2801043 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/wrapper-validation-action@v3 - name: Set up JDK 11 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index bd64e01bab..5e90decd21 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/wrapper-validation-action@v3 - name: Set up JDK 11 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d2a133472..21b31d32ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/wrapper-validation-action@v3 - name: Set up JDK 11 uses: actions/setup-java@v4 with: