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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class ExampleSubscriptionService : Subscription {

@GraphQLDescription("Returns stream of errors")
fun flowOfErrors(): Publisher<DataFetcherResult<String?>> {
val dfr: DataFetcherResult<String?> = DataFetcherResult.newResult<String?>()
val dfr: DataFetcherResult<String?> = DataFetcherResult.Builder<String?>()
.data(null)
.error(GraphqlErrorException.newErrorException().cause(Exception("error thrown")).build())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import java.util.concurrent.CompletableFuture
class DataAndErrorsQuery : Query {

fun returnDataAndErrors(): DataFetcherResult<String?> {
return DataFetcherResult.newResult<String>()
return DataFetcherResult.Builder<String?>()
.data("Hello from data fetcher")
.error(getError())
.build()
}

fun completableFutureDataAndErrors(): CompletableFuture<DataFetcherResult<String?>> {
val dataFetcherResult = DataFetcherResult.newResult<String>()
val dataFetcherResult = DataFetcherResult.Builder<String?>()
.data("Hello from data fetcher")
.error(getError())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class SimpleSubscription : Subscription {

@GraphQLDescription("Returns stream of errors")
fun flowOfErrors(): Publisher<DataFetcherResult<String?>> {
val dfr: DataFetcherResult<String?> = DataFetcherResult.newResult<String?>()
val dfr: DataFetcherResult<String?> = DataFetcherResult.Builder<String?>()
.data(null)
.error(GraphqlErrorException.newErrorException().cause(Exception("error thrown")).build())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,26 @@ import graphql.ExecutionInput
import graphql.execution.preparsed.PreparsedDocumentEntry
import graphql.execution.preparsed.persisted.PersistedQueryCache
import graphql.execution.preparsed.persisted.PersistedQueryCacheMiss
import graphql.execution.preparsed.persisted.PersistedQuerySupport
import java.util.concurrent.CompletableFuture

interface AutomaticPersistedQueriesCache : PersistedQueryCache {
override fun getPersistedQueryDocumentAsync(
persistedQueryId: Any,
executionInput: ExecutionInput,
onCacheMiss: PersistedQueryCacheMiss
): CompletableFuture<PreparsedDocumentEntry> =
getOrElse(persistedQueryId.toString(), executionInput) {
onCacheMiss.apply(executionInput.query)
): CompletableFuture<PreparsedDocumentEntry> {
// In graphql-java 25+, ExecutionInput substitutes PERSISTED_QUERY_MARKER for a null/empty
// query string when an APQ extension is present, to satisfy the invariant that getQuery()
// is never null. We must treat it the same as a blank query — both signal that no query
// text was sent with this request and a cache miss (PersistedQueryNotFound) should follow.
val queryText = executionInput.query.takeUnless {
it.isBlank() || it == PersistedQuerySupport.PERSISTED_QUERY_MARKER
}
return getOrElse(persistedQueryId.toString(), executionInput) {
onCacheMiss.apply(queryText ?: "")
}
}

/**
* Get the [PreparsedDocumentEntry] associated with the [key] from the cache.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class SyncExecutionExhaustedState(
fun beginExecution(
parameters: InstrumentationExecutionParameters
): InstrumentationContext<ExecutionResult> {
executions.computeIfAbsent(parameters.executionInput.executionId) {
executions.computeIfAbsent(parameters.executionInput.executionIdNonNull) {
ExecutionInputState(parameters.executionInput)
}
return object : SimpleInstrumentationContext<ExecutionResult>() {
Expand Down Expand Up @@ -93,7 +93,7 @@ class SyncExecutionExhaustedState(
fun beginRecursiveExecution(
parameters: InstrumentationExecutionStrategyParameters
) {
val executionId = parameters.executionContext.executionInput.executionId
val executionId = parameters.executionContext.executionInput.executionIdNonNull
executions.computeIfPresent(executionId) { _, executionState ->
val executionStrategyParameters = parameters.executionStrategyParameters

Expand All @@ -117,7 +117,7 @@ class SyncExecutionExhaustedState(
fun beginFieldFetching(
parameters: InstrumentationFieldFetchParameters
): FieldFetchingInstrumentationContext {
val executionId = parameters.executionContext.executionInput.executionId
val executionId = parameters.executionContext.executionInput.executionIdNonNull
val field = parameters.executionStepInfo.field.singleField
val fieldExecutionStrategyPath = parameters.executionStepInfo.path.parent
val fieldGraphQLType = parameters.executionStepInfo.unwrappedNonNullType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,38 @@

package com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion

import com.expediagroup.graphql.dataloader.instrumentation.exceptions.MissingInstrumentationStateException
import com.expediagroup.graphql.dataloader.instrumentation.extensions.dispatchIfNeeded
import com.expediagroup.graphql.dataloader.instrumentation.fixture.DataLoaderInstrumentationStrategy
import com.expediagroup.graphql.dataloader.instrumentation.fixture.AstronautGraphQL
import com.expediagroup.graphql.dataloader.instrumentation.fixture.ProductGraphQL
import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.state.SyncExecutionExhaustedState
import graphql.ExecutionInput
import graphql.ExecutionResult
import graphql.GraphQLContext
import graphql.execution.ExecutionContext
import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext
import graphql.execution.instrumentation.FieldFetchingInstrumentationContext
import graphql.execution.instrumentation.InstrumentationContext
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.DataFetchingEnvironment
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.dataloader.DataLoaderRegistry
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

class GraphQLSyncExecutionExhaustedDataLoaderDispatcherTest {
Expand All @@ -47,6 +69,171 @@ class GraphQLSyncExecutionExhaustedDataLoaderDispatcherTest {
clearAllMocks()
}

@Test
fun `beginExecution returns null when sync state is not present in context`() {
val parameters = mockk<InstrumentationExecutionParameters>()
every { parameters.graphQLContext } returns GraphQLContext.newContext().build()

val result = graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginExecution(parameters, null)

assertNull(result)
}

@Test
fun `beginExecution delegates to sync state when present`() {
val syncState = mockk<SyncExecutionExhaustedState>()
val delegatedContext = mockk<InstrumentationContext<ExecutionResult>>()
val parameters = mockk<InstrumentationExecutionParameters>()

every { parameters.graphQLContext } returns GraphQLContext.newContext()
.of(SyncExecutionExhaustedState::class, syncState)
.build()
every { syncState.beginExecution(parameters) } returns delegatedContext

val result = graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginExecution(parameters, null)

assertSame(delegatedContext, result)
}

@Test
fun `beginExecutionStrategy returns null and skips delegation when sync state is missing`() {
val executionContext = mockExecutionContext(GraphQLContext.newContext().build())
val parameters = mockk<InstrumentationExecutionStrategyParameters>()
every { parameters.executionContext } returns executionContext

val result = graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginExecutionStrategy(parameters, null)

assertNull(result)
}

@Test
fun `beginExecutionStrategy delegates recursive execution when sync state is present`() {
val syncState = mockk<SyncExecutionExhaustedState>()
every { syncState.beginRecursiveExecution(any()) } just runs

val executionContext = mockExecutionContext(
GraphQLContext.newContext().of(SyncExecutionExhaustedState::class, syncState).build()
)
val parameters = mockk<InstrumentationExecutionStrategyParameters>()
every { parameters.executionContext } returns executionContext

val result: ExecutionStrategyInstrumentationContext? =
graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginExecutionStrategy(parameters, null)

assertNull(result)
verify(exactly = 1) { syncState.beginRecursiveExecution(parameters) }
}

@Test
fun `beginExecuteObject returns null and skips delegation when sync state is missing`() {
val executionContext = mockExecutionContext(GraphQLContext.newContext().build())
val parameters = mockk<InstrumentationExecutionStrategyParameters>()
every { parameters.executionContext } returns executionContext

val result = graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginExecuteObject(parameters, null)

assertNull(result)
}

@Test
fun `beginExecuteObject delegates recursive execution when sync state is present`() {
val syncState = mockk<SyncExecutionExhaustedState>()
every { syncState.beginRecursiveExecution(any()) } just runs

val executionContext = mockExecutionContext(
GraphQLContext.newContext().of(SyncExecutionExhaustedState::class, syncState).build()
)
val parameters = mockk<InstrumentationExecutionStrategyParameters>()
every { parameters.executionContext } returns executionContext

val result = graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginExecuteObject(parameters, null)

assertNull(result)
verify(exactly = 1) { syncState.beginRecursiveExecution(parameters) }
}

@Test
fun `beginFieldFetching returns null when sync state is missing`() {
val executionContext = mockExecutionContext(GraphQLContext.newContext().build())
val parameters = mockk<InstrumentationFieldFetchParameters>()
every { parameters.executionContext } returns executionContext

val result = graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginFieldFetching(parameters, null)

assertNull(result)
}

@Test
fun `beginFieldFetching delegates when sync state is present`() {
val syncState = mockk<SyncExecutionExhaustedState>()
val delegatedContext = mockk<FieldFetchingInstrumentationContext>()
every { syncState.beginFieldFetching(any()) } returns delegatedContext

val executionContext = mockExecutionContext(
GraphQLContext.newContext().of(SyncExecutionExhaustedState::class, syncState).build()
)
val parameters = mockk<InstrumentationFieldFetchParameters>()
every { parameters.executionContext } returns executionContext

val result = graphQLSyncExecutionExhaustedDataLoaderDispatcher.beginFieldFetching(parameters, null)

assertSame(delegatedContext, result)
verify(exactly = 1) { syncState.beginFieldFetching(parameters) }
}

@Test
fun `dispatchIfNeeded throws when sync execution state is missing`() {
val dataLoaderRegistry = mockk<DataLoaderRegistry>(relaxed = true)
val environment = mockk<DataFetchingEnvironment>()
every { environment.dataLoaderRegistry } returns dataLoaderRegistry
every { environment.graphQlContext } returns GraphQLContext.newContext().build()

assertFailsWith<MissingInstrumentationStateException> {
CompletableFuture.completedFuture("value").dispatchIfNeeded(environment)
}

verify(exactly = 0) { dataLoaderRegistry.dispatchAll() }
}

@Test
fun `dispatchIfNeeded dispatches all when chained loads happened and executions are exhausted`() {
val dataLoaderRegistry = mockk<DataLoaderRegistry>(relaxed = true)
val syncState = mockk<SyncExecutionExhaustedState>()
every { syncState.dataLoadersLoadInvokedAfterDispatchAll() } returns true
every { syncState.allSyncExecutionsExhausted() } returns true

val environment = mockk<DataFetchingEnvironment>()
every { environment.dataLoaderRegistry } returns dataLoaderRegistry
every { environment.graphQlContext } returns GraphQLContext.newContext()
.of(SyncExecutionExhaustedState::class, syncState)
.build()

val future = CompletableFuture.completedFuture("value")

val result = future.dispatchIfNeeded(environment)

assertSame(future, result)
verify(exactly = 1) { dataLoaderRegistry.dispatchAll() }
}

@Test
fun `dispatchIfNeeded does not dispatch when executions are not exhausted`() {
val dataLoaderRegistry = mockk<DataLoaderRegistry>(relaxed = true)
val syncState = mockk<SyncExecutionExhaustedState>()
every { syncState.dataLoadersLoadInvokedAfterDispatchAll() } returns true
every { syncState.allSyncExecutionsExhausted() } returns false

val environment = mockk<DataFetchingEnvironment>()
every { environment.dataLoaderRegistry } returns dataLoaderRegistry
every { environment.graphQlContext } returns GraphQLContext.newContext()
.of(SyncExecutionExhaustedState::class, syncState)
.build()

CompletableFuture.completedFuture("value").dispatchIfNeeded(environment)

verify(exactly = 0) { dataLoaderRegistry.dispatchAll() }
}

@Test
fun `Instrumentation should batch transactions on async top level fields`() {
val queries = listOf(
Expand Down Expand Up @@ -561,7 +748,7 @@ class GraphQLSyncExecutionExhaustedDataLoaderDispatcherTest {
"""mutation { createAstronaut(name: "spaceMan") { id name } }"""
)

val (results, dataLoaderRegistry, graphQLContext) = AstronautGraphQL.executeOperations(
val (results, _, graphQLContext) = AstronautGraphQL.executeOperations(
astronautGraphQL,
queries,
DataLoaderInstrumentationStrategy.SYNC_EXHAUSTION
Expand Down Expand Up @@ -634,4 +821,14 @@ class GraphQLSyncExecutionExhaustedDataLoaderDispatcherTest {
graphQLContext.get(DataLoaderRegistry::class)
}
}

private fun mockExecutionContext(context: GraphQLContext): ExecutionContext {
val operationDefinition = mockk<OperationDefinition>()
every { operationDefinition.operation } returns OperationDefinition.Operation.QUERY

val executionContext = mockk<ExecutionContext>()
every { executionContext.operationDefinition } returns operationDefinition
every { executionContext.graphQLContext } returns context
return executionContext
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,35 @@ private class LinkImportCoercing : Coercing<LinkImport, Any> {

override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): LinkImport = when (input) {
is LinkImport -> input
is StringValue -> LinkImport(name = input.value, `as` = input.value)
is ObjectValue -> {
val nameValue = input.objectFields.firstOrNull { it.name == "name" }?.value as? StringValue ?: throw CoercingParseValueException("Cannot parse $input to LinkImport")
val namespacedValue = input.objectFields.firstOrNull { it.name == "as" }?.value as? StringValue
LinkImport(name = nameValue.value, `as` = namespacedValue?.value ?: nameValue.value)
}
is StringValue -> parseStringValue(input) { msg -> CoercingParseValueException(msg) }
is ObjectValue -> parseObjectValue(input) { msg -> CoercingParseValueException(msg) }
else -> throw CoercingParseValueException(
"Cannot parse $input to LinkImport. Expected AST type of `StringValue` or `ObjectValue` but was ${input.javaClass.simpleName} "
)
}

override fun parseLiteral(input: Value<*>, variables: CoercedVariables, graphQLContext: GraphQLContext, locale: Locale): LinkImport =
when (input) {
is StringValue -> LinkImport(name = input.value, `as` = input.value)
is ObjectValue -> {
val nameValue = input.objectFields.firstOrNull { it.name == "name" }?.value as? StringValue ?: throw CoercingParseLiteralException("Cannot parse $input to LinkImport")
val namespacedValue = input.objectFields.firstOrNull { it.name == "as" }?.value as? StringValue
LinkImport(name = nameValue.value, `as` = namespacedValue?.value ?: nameValue.value)
}
is StringValue -> parseStringValue(input) { msg -> CoercingParseLiteralException(msg) }
is ObjectValue -> parseObjectValue(input) { msg -> CoercingParseLiteralException(msg) }
else -> throw CoercingParseLiteralException(
"Cannot parse $input to LinkImport. Expected AST type of `StringValue` or `ObjectValue` but was ${input.javaClass.simpleName} "
)
}

private fun parseStringValue(input: StringValue, exception: (String) -> RuntimeException): LinkImport {
val value = input.value ?: throw exception("Cannot parse StringValue to LinkImport: string value was null")
return LinkImport(name = value, `as` = value)
}

private fun parseObjectValue(input: ObjectValue, exception: (String) -> RuntimeException): LinkImport {
val nameValue = input.objectFields.firstOrNull { it.name == "name" }?.value as? StringValue
?: throw exception("Cannot parse $input to LinkImport: missing or non-string 'name' field")
val name = nameValue.value ?: throw exception("Cannot parse $input to LinkImport: 'name' field StringValue was null")
val alias = (input.objectFields.firstOrNull { it.name == "as" }?.value as? StringValue)?.value ?: name
return LinkImport(name = name, `as` = alias)
}

override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> {
return when (input) {
is String -> StringValue.newStringValue(input).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private object AnyCoercing : Coercing<Any, Any> {
override fun parseLiteral(input: Value<*>, variables: CoercedVariables, graphQLContext: GraphQLContext, locale: Locale): Any =
when (input) {
is FloatValue -> input.value
is StringValue -> input.value
is StringValue -> input.value ?: throw CoercingParseLiteralException("Cannot parse null StringValue value to Any scalar")
is IntValue -> input.value
is BooleanValue -> input.isValue
is EnumValue -> input.name
Expand Down
Loading
Loading