diff --git a/lib/src/main/java/graphql/nadel/engine/transform/artificial/NadelAliasHelper.kt b/lib/src/main/java/graphql/nadel/engine/transform/artificial/NadelAliasHelper.kt index 92d576ad6..034fdf54d 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/artificial/NadelAliasHelper.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/artificial/NadelAliasHelper.kt @@ -58,6 +58,7 @@ class NadelAliasHelper private constructor(private val alias: String) { fun getQueryPath( path: NadelQueryPath, ): NadelQueryPath { + // todo: why not just clone and set first index here? return path.mapIndexed { index, segment -> when (index) { 0 -> getResultKey(segment) diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationFieldsBuilder.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationFieldsBuilder.kt index a93ead019..fa6c6bae9 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationFieldsBuilder.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationFieldsBuilder.kt @@ -61,6 +61,22 @@ internal object NadelHydrationFieldsBuilder { userContext = userContext, ) + return makeBatchActorQueries( + executionBlueprint = executionBlueprint, + instruction = instruction, + aliasHelper = aliasHelper, + hydratedField = hydratedField, + argBatches = argBatches, + ) + } + + fun makeBatchActorQueries( + executionBlueprint: NadelOverallExecutionBlueprint, + instruction: NadelBatchHydrationFieldInstruction, + aliasHelper: NadelAliasHelper, + hydratedField: ExecutableNormalizedField, + argBatches: List>, + ): List { val actorFieldOverallObjectTypeNames = getActorFieldOverallObjectTypenames(instruction, executionBlueprint) val fieldChildren = deepClone(fields = hydratedField.children) .mapNotNull { childField -> diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt index 78c47eede..ce35b1fa6 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt @@ -9,7 +9,6 @@ import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy import graphql.nadel.engine.transform.artificial.NadelAliasHelper import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.engine.transform.result.json.JsonNodeExtractor -import graphql.nadel.engine.util.AnyAstValue import graphql.nadel.engine.util.emptyOrSingle import graphql.nadel.engine.util.flatten import graphql.nadel.engine.util.javaValueToAstValue diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt index 3ece854ed..77048ecc4 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt @@ -37,7 +37,7 @@ internal object NadelBatchHydrationInputBuilder { return batchArgs.map { nonBatchArgs + it } } - private fun getNonBatchInputValues( + internal fun getNonBatchInputValues( instruction: NadelBatchHydrationFieldInstruction, hydrationField: ExecutableNormalizedField, ): Map { diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrationInputBuilder.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrationInputBuilder.kt new file mode 100644 index 000000000..d92e2fa37 --- /dev/null +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrationInputBuilder.kt @@ -0,0 +1,65 @@ +package graphql.nadel.engine.transform.hydration.batch + +import graphql.nadel.engine.blueprint.NadelBatchHydrationFieldInstruction +import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy +import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef +import graphql.nadel.engine.transform.hydration.batch.NadelBatchHydrationInputBuilder.getBatchInputDef +import graphql.nadel.engine.transform.hydration.batch.NadelBatchHydrationInputBuilder.getNonBatchInputValues +import graphql.nadel.engine.transform.result.json.JsonNode +import graphql.nadel.engine.util.javaValueToAstValue +import graphql.nadel.hooks.NadelExecutionHooks +import graphql.normalized.ExecutableNormalizedField +import graphql.normalized.NormalizedInputValue +import graphql.schema.GraphQLTypeUtil + +/** + * todo: did I follow this README when forking? + * + * README + * + * Please ensure that the batch arguments are ordered according to the input. + * This is required for [NadelBatchHydrationMatchStrategy.MatchIndex]. + */ +internal object NadelNewBatchHydrationInputBuilder { + fun getInputValueBatches( + hooks: NadelExecutionHooks, + userContext: Any?, + instruction: NadelBatchHydrationFieldInstruction, + hydrationField: ExecutableNormalizedField, + sourceIds: List, + ): List> { + val nonBatchArgs = getNonBatchInputValues(instruction, hydrationField) + val batchArgs = getBatchInputValues(instruction, sourceIds, hooks, userContext) + + return batchArgs.map { nonBatchArgs + it } + } + + private fun getBatchInputValues( + instruction: NadelBatchHydrationFieldInstruction, + sourceIds: List, + hooks: NadelExecutionHooks, + userContext: Any?, + ): List> { + val batchSize = instruction.batchSize + + val (batchInputDef, batchInputValueSource) = getBatchInputDef(instruction) ?: return emptyList() + val actorBatchArgDef = instruction.actorFieldDef.getArgument(batchInputDef.name) + + val partitionArgumentList = hooks.partitionBatchHydrationArgumentList( + argumentValues = sourceIds.map { it.value }, + instruction = instruction, + userContext = userContext, + ) + + return partitionArgumentList + .flatMap { + it.chunked(size = batchSize) + } + .map { chunk -> + batchInputDef to NormalizedInputValue( + GraphQLTypeUtil.simplePrint(actorBatchArgDef.type), + javaValueToAstValue(chunk), + ) + } + } +} diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt index 7c847b24c..22459f3b2 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt @@ -1,19 +1,284 @@ package graphql.nadel.engine.transform.hydration.batch import graphql.nadel.NextgenEngine +import graphql.nadel.ServiceExecutionHydrationDetails +import graphql.nadel.ServiceExecutionResult +import graphql.nadel.engine.blueprint.NadelBatchHydrationFieldInstruction import graphql.nadel.engine.blueprint.NadelOverallExecutionBlueprint +import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy +import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef.ValueSource +import graphql.nadel.engine.transform.artificial.NadelAliasHelper +import graphql.nadel.engine.transform.getInstructionsForNode +import graphql.nadel.engine.transform.hydration.NadelHydrationFieldsBuilder import graphql.nadel.engine.transform.hydration.batch.NadelBatchHydrationTransform.State import graphql.nadel.engine.transform.result.NadelResultInstruction +import graphql.nadel.engine.transform.result.NadelResultKey import graphql.nadel.engine.transform.result.json.JsonNode +import graphql.nadel.engine.transform.result.json.JsonNodeExtractor +import graphql.nadel.engine.util.MutableJsonMap +import graphql.nadel.engine.util.flatten +import graphql.nadel.engine.util.getField +import graphql.nadel.engine.util.isList +import graphql.nadel.engine.util.makeFieldCoordinates +import graphql.nadel.engine.util.singleOfType +import graphql.nadel.engine.util.unwrapNonNull +import graphql.schema.FieldCoordinates +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope internal class NadelNewBatchHydrator( private val engine: NextgenEngine, ) { - fun hydrate( + private data class ObjectIdentifier( + val data: Any?, + ) + + /** + * todo: add validation that repeated directives must use the same $source object unless there is only one input + */ + suspend fun hydrate( state: State, executionBlueprint: NadelOverallExecutionBlueprint, parentNodes: List, ): List { - throw UnsupportedOperationException() + val parentNodeHydrationSetups = getParentNodeHydrationSetup( + state, + executionBlueprint, + parentNodes, + ) + + val sourceIdsByInstruction = parentNodeHydrationSetups + .flatMap { + it.sourceIds + } + .groupBy( + keySelector = { (_, instruction) -> + instruction + }, + valueTransform = { (sourceId, _) -> + sourceId + }, + ) + + val indexedResults = sourceIdsByInstruction + .mapValues { (instruction, sourceIds) -> + val results = executeQueries( + state = state, + executionBlueprint = executionBlueprint, + instruction = instruction, + sourceIds = sourceIds, + ) + + indexResults(state.aliasHelper, instruction, results) + } + + val isHydratedFieldListOutput = executionBlueprint.engineSchema + .getField( + makeFieldCoordinates( + typeName = state.hydratedField.objectTypeNames.first(), + fieldName = state.hydratedField.name, + ) + )!!.type.unwrapNonNull().isList + + return parentNodeHydrationSetups + .map { (parentNode, sourceIdsPairedWithInstruction) -> + fun extractNode(sourceId: JsonNode, instruction: NadelBatchHydrationFieldInstruction): JsonNode? { + return indexedResults[instruction]!![ObjectIdentifier(sourceId.value)] + } + + val value: Any? = if (isHydratedFieldListOutput) { + sourceIdsPairedWithInstruction + .map { (sourceId, instruction) -> + extractNode(sourceId, instruction)?.value + } + } else { + val (sourceId, instruction) = sourceIdsPairedWithInstruction.single() + extractNode(sourceId, instruction)?.value + } + + NadelResultInstruction.Set( + subject = parentNode, + key = NadelResultKey(state.hydratedField.resultKey), + newValue = JsonNode(value), + ) + } + } + + private fun indexResults( + aliasHelper: NadelAliasHelper, + instruction: NadelBatchHydrationFieldInstruction, + results: List, + ): Map { + return when (val strategy = instruction.batchHydrationMatchStrategy) { + is NadelBatchHydrationMatchStrategy.MatchIndex -> { + throw UnsupportedOperationException("todo") + } + is NadelBatchHydrationMatchStrategy.MatchObjectIdentifier -> { + results + .flatMap { result -> + JsonNodeExtractor.getNodesAt(result.data, instruction.queryPathToActorField, flatten = true) + } + .groupBy { node -> + @Suppress("UNCHECKED_CAST") + ObjectIdentifier( + // Remove result ID after using it to create this index to stop it showing up in end result + (node.value as MutableJsonMap).remove(aliasHelper.getResultKey(strategy.resultId)), + ) + } + .mapValues { (_, values) -> + // todo: stop doing stupid here + values.single() + } + } + is NadelBatchHydrationMatchStrategy.MatchObjectIdentifiers -> { + throw UnsupportedOperationException("todo") + } + } + } + + private suspend fun executeQueries( + state: State, + executionBlueprint: NadelOverallExecutionBlueprint, + instruction: NadelBatchHydrationFieldInstruction, + sourceIds: List, + ): List { + val argBatches = NadelNewBatchHydrationInputBuilder.getInputValueBatches( + hooks = state.executionContext.hooks, + userContext = state.executionContext.userContext, + instruction = instruction, + hydrationField = state.hydratedField, + sourceIds = sourceIds, + ) + + val queries = NadelHydrationFieldsBuilder + .makeBatchActorQueries( + executionBlueprint = executionBlueprint, + instruction = instruction, + aliasHelper = state.aliasHelper, + hydratedField = state.hydratedField, + argBatches = argBatches, + ) + + return coroutineScope { + queries + .map { query -> + async { // This async executes the batches in parallel i.e. executes hydration as Deferred/Future + val hydrationSourceService = executionBlueprint.getServiceOwning(instruction.location)!! + val hydrationActorField = + FieldCoordinates.coordinates(instruction.actorFieldContainer, instruction.actorFieldDef) + + val serviceHydrationDetails = ServiceExecutionHydrationDetails( + timeout = instruction.timeout, + batchSize = instruction.batchSize, + hydrationSourceService = hydrationSourceService, + hydrationSourceField = instruction.location, + hydrationActorField = hydrationActorField, + fieldPath = state.hydratedField.listOfResultKeys, + ) + engine.executeTopLevelField( + service = instruction.actorService, + topLevelField = query, + executionContext = state.executionContext, + serviceHydrationDetails = serviceHydrationDetails, + ) + } + } + }.awaitAll() + } + + private data class ParentNodeHydrationSetup( + val parentNode: JsonNode, + val sourceIds: List>, + ) + + private fun getParentNodeHydrationSetup( + state: State, + executionBlueprint: NadelOverallExecutionBlueprint, + parentNodes: List, + ): List { + return parentNodes + .map { parentNode -> + val instructions = state.instructionsByObjectTypeNames.getInstructionsForNode( + executionBlueprint = executionBlueprint, + service = state.hydratedFieldService, + aliasHelper = state.aliasHelper, + parentNode = parentNode, + ) + + val sourceIdsPairedWithInstructions = getInstructionParingForSourceIds( + state = state, + executionBlueprint = executionBlueprint, + parentNode = parentNode, + instructions = instructions, + ) + + ParentNodeHydrationSetup( + parentNode, + sourceIdsPairedWithInstructions, + ) + } + } + + private fun getInstructionParingForSourceIds( + state: State, + executionBlueprint: NadelOverallExecutionBlueprint, + parentNode: JsonNode, + instructions: List, + ): List> { + val coords = makeFieldCoordinates( + typeName = state.hydratedField.objectTypeNames.first(), + fieldName = state.hydratedField.name, + ) + + return if (executionBlueprint.engineSchema.getField(coords)!!.type.unwrapNonNull().isList) { + val fieldSource = instructions + .first() + .actorInputValueDefs + .asSequence() + .map { + it.valueSource + } + .singleOfType() + + // todo: move this to validation + instructions + .forEach { instruction -> + instruction.actorInputValueDefs.single { arg -> + arg.valueSource == fieldSource + } + } + + extractValues(parentNode, fieldSource, state.aliasHelper) + .map { sourceId -> + // todo: handle null here + val instruction = state.executionContext.hooks.getHydrationInstruction( + instructions = instructions, + sourceId = sourceId, + userContext = state.executionContext.userContext, + )!! + + sourceId to instruction + } + } else { + throw UnsupportedOperationException("todo") + } + } + + private fun extractValues( + parentNode: JsonNode, + valueSource: ValueSource.FieldResultValue, + aliasHelper: NadelAliasHelper, + ): List { + val resultPath = aliasHelper.getQueryPath(valueSource.queryPathToField) + @Suppress("DEPRECATION") // todo: maybe un-deprecate this or move to new JsonNodes + return JsonNodeExtractor.getNodesAt(parentNode, resultPath, flatten = true) + .asSequence() + .map { it.value } + .flatten(recursively = true) + .map { + JsonNode(it) + } + .toList() } } diff --git a/lib/src/main/java/graphql/nadel/engine/transform/result/NadelResultTransformer.kt b/lib/src/main/java/graphql/nadel/engine/transform/result/NadelResultTransformer.kt index b42f239db..e13f25e91 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/result/NadelResultTransformer.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/result/NadelResultTransformer.kt @@ -24,7 +24,7 @@ internal class NadelResultTransformer(private val executionBlueprint: NadelOvera service: Service, result: ServiceExecutionResult, ): ServiceExecutionResult { - val nodes = JsonNodes(result.data, executionContext.hints) + val nodes = JsonNodes(result.data) val deferredInstructions = ArrayList>>() diff --git a/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt b/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt index 65c67e115..fd7d85c14 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/result/json/JsonNodes.kt @@ -1,6 +1,5 @@ package graphql.nadel.engine.transform.result.json -import graphql.nadel.NadelExecutionHints import graphql.nadel.engine.transform.query.NadelQueryPath import graphql.nadel.engine.util.AnyList import graphql.nadel.engine.util.AnyMap @@ -8,11 +7,10 @@ import graphql.nadel.engine.util.JsonMap import java.util.concurrent.ConcurrentHashMap /** - * Utility class to extract data out of the given + * Utility class to extract data out of the given [data]. */ class JsonNodes( private val data: JsonMap, - private val executionFlags: NadelExecutionHints, ) { private val nodes = ConcurrentHashMap>() diff --git a/lib/src/main/java/graphql/nadel/engine/util/CollectionUtil.kt b/lib/src/main/java/graphql/nadel/engine/util/CollectionUtil.kt index ebf2779c8..3f5cf996b 100644 --- a/lib/src/main/java/graphql/nadel/engine/util/CollectionUtil.kt +++ b/lib/src/main/java/graphql/nadel/engine/util/CollectionUtil.kt @@ -19,6 +19,20 @@ inline fun Collection<*>.singleOfType(predicate: (T) -> Boolean = { return singleOfTypeOrNull(predicate)!! } +/** + * Like [singleOrNull] but the single item must be of type [T]. + */ +inline fun Sequence<*>.singleOfTypeOrNull(predicate: (T) -> Boolean = { true }): T? { + return singleOrNull { it is T && predicate(it) } as T? +} + +/** + * Like [singleOrNull] but the single item must be of type [T]. + */ +inline fun Sequence<*>.singleOfType(predicate: (T) -> Boolean = { true }): T { + return singleOfTypeOrNull(predicate)!! +} + inline fun Iterable.strictAssociateBy(crossinline keyExtractor: (E) -> K): Map { return mapFrom( map { diff --git a/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt b/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt index b7a6e495c..87d33f109 100644 --- a/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt +++ b/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt @@ -51,6 +51,14 @@ interface NadelExecutionHooks { return instructions.single() } + fun getHydrationInstruction( + instructions: List, + sourceId: JsonNode, + userContext: Any?, + ): T? { + throw UnsupportedOperationException() + } + /** * This method should be used when the list of hydration arguments needs to be split in batches. The batches will be * executed separately. One example is partitioning the list of arguments when different arguments cannot be diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/`new-batching-multiple-source-ids-going-to-different-services`.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/`new-batching-multiple-source-ids-going-to-different-services`.kt new file mode 100644 index 000000000..f460ccb07 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/`new-batching-multiple-source-ids-going-to-different-services`.kt @@ -0,0 +1,37 @@ +package graphql.nadel.tests.hooks + +import graphql.nadel.Nadel +import graphql.nadel.NadelExecutionHints +import graphql.nadel.engine.blueprint.NadelGenericHydrationInstruction +import graphql.nadel.engine.transform.result.json.JsonNode +import graphql.nadel.hooks.NadelExecutionHooks +import graphql.nadel.tests.EngineTestHook +import graphql.nadel.tests.UseHook + +@UseHook +class `new-batching-multiple-source-ids-going-to-different-services` : EngineTestHook { + override fun makeExecutionHints(builder: NadelExecutionHints.Builder): NadelExecutionHints.Builder { + return super.makeExecutionHints(builder) + .newBatchHydrationGrouping { true } + } + + override fun makeNadel(builder: Nadel.Builder): Nadel.Builder { + return super.makeNadel(builder) + .executionHooks( + object : NadelExecutionHooks { + override fun getHydrationInstruction( + instructions: List, + sourceId: JsonNode, + userContext: Any?, + ): T { + val type = (sourceId.value as String).substringBefore("/") + + return instructions + .first { + it.actorService.name.startsWith(type, ignoreCase = true) + } + } + }, + ) + } +} diff --git a/test/src/test/resources/fixtures/hydration/new batching/new-batching-multiple-source-ids-going-to-different-services.yml b/test/src/test/resources/fixtures/hydration/new batching/new-batching-multiple-source-ids-going-to-different-services.yml new file mode 100644 index 000000000..17bcbbc28 --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/new batching/new-batching-multiple-source-ids-going-to-different-services.yml @@ -0,0 +1,299 @@ +name: "new batching multiple source ids going to different services" +enabled: true +overallSchema: + # language=GraphQL + activity: | + type Query { + activity: [Activity] + } + union ActivityContent = Issue | Comment + type Activity { + id: ID! + contentIds: [ID!]! + content: [ActivityContent] + @hydrated( + service: "comments" + field: "commentsByIds" + arguments: [ + {name: "ids" value: "$source.contentIds"} + ] + ) + @hydrated( + service: "issues" + field: "issuesByIds" + arguments: [ + {name: "ids" value: "$source.contentIds"} + ] + ) + } + # language=GraphQL + issues: | + type Query { + issuesByIds(ids: [ID!]!): [Issue!] + } + type Issue { + id: ID! + title: String + } + # language=GraphQL + comments: | + type Query { + commentsByIds(ids: [ID!]!): [Comment!] + } + type Comment { + id: ID! + content: String + } +underlyingSchema: + # language=GraphQL + activity: | + type Query { + activity: [Activity] + } + type Activity { + id: ID! + contentIds: [ID!]! + } + # language=GraphQL + issues: | + type Query { + issuesByIds(ids: [ID!]!): [Issue!] + } + type Issue { + id: ID! + title: String + } + # language=GraphQL + comments: | + type Query { + commentsByIds(ids: [ID!]!): [Comment!] + } + type Comment { + id: ID! + content: String + } +# language=GraphQL +query: | + { + activity { + content { + __typename + ... on Issue { + id + title + } + ... on Comment { + id + content + } + } + } + } +variables: { } +serviceCalls: + - serviceName: "activity" + request: + # language=GraphQL + query: | + { + activity { + __typename__batch_hydration__content: __typename + batch_hydration__content__contentIds: contentIds + batch_hydration__content__contentIds: contentIds + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "activity": [ + { + "__typename__batch_hydration__content": "Activity", + "batch_hydration__content__contentIds": [ + "issue/4000", + "comment/5000", + "comment/6000" + ] + }, + { + "__typename__batch_hydration__content": "Activity", + "batch_hydration__content__contentIds": [ + "issue/8080" + ] + }, + { + "__typename__batch_hydration__content": "Activity", + "batch_hydration__content__contentIds": [ + "issue/7496", + "comment/9001" + ] + }, + { + "__typename__batch_hydration__content": "Activity", + "batch_hydration__content__contentIds": [ + "issue/1234", + "comment/1234" + ] + } + ] + }, + "extensions": {} + } + - serviceName: "issues" + request: + # language=GraphQL + query: | + { + issuesByIds(ids: ["issue/4000", "issue/8080", "issue/7496", "issue/1234"]) { + __typename + id + batch_hydration__content__id: id + title + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "issuesByIds": [ + { + "__typename": "Issue", + "id": "issue/4000", + "batch_hydration__content__id": "issue/4000", + "title": "Four Thousand" + }, + { + "__typename": "Issue", + "id": "issue/8080", + "batch_hydration__content__id": "issue/8080", + "title": "Eighty Eighty" + }, + { + "__typename": "Issue", + "id": "issue/7496", + "batch_hydration__content__id": "issue/7496", + "title": "Seven Four Nine Six" + }, + { + "__typename": "Issue", + "id": "issue/1234", + "batch_hydration__content__id": "issue/1234", + "title": "One Two Three Four" + } + ] + }, + "extensions": {} + } + - serviceName: "comments" + request: + # language=GraphQL + query: | + { + commentsByIds(ids: ["comment/5000", "comment/6000", "comment/9001", "comment/1234"]) { + __typename + content + id + batch_hydration__content__id: id + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "commentsByIds": [ + { + "__typename": "Comment", + "id": "comment/5000", + "batch_hydration__content__id": "comment/5000", + "content": "Five Thousand" + }, + { + "__typename": "Comment", + "id": "comment/6000", + "batch_hydration__content__id": "comment/6000", + "content": "Six Thousand" + }, + { + "__typename": "Comment", + "id": "comment/9001", + "batch_hydration__content__id": "comment/9001", + "content": "It's over 9000" + }, + { + "__typename": "Comment", + "id": "comment/1234", + "batch_hydration__content__id": "comment/1234", + "content": "One Two Three Four" + } + ] + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "activity": [ + { + "content": [ + { + "__typename": "Issue", + "id": "issue/4000", + "title": "Four Thousand" + }, + { + "__typename": "Comment", + "id": "comment/5000", + "content": "Five Thousand" + }, + { + "__typename": "Comment", + "id": "comment/6000", + "content": "Six Thousand" + } + ] + }, + { + "content": [ + { + "__typename": "Issue", + "id": "issue/8080", + "title": "Eighty Eighty" + } + ] + }, + { + "content": [ + { + "__typename": "Issue", + "id": "issue/7496", + "title": "Seven Four Nine Six" + }, + { + "__typename": "Comment", + "id": "comment/9001", + "content": "It's over 9000" + } + ] + }, + { + "content": [ + { + "__typename": "Issue", + "id": "issue/1234", + "title": "One Two Three Four" + }, + { + "__typename": "Comment", + "id": "comment/1234", + "content": "One Two Three Four" + } + ] + } + ] + }, + "errors": [] + }