diff --git a/.github/workflows/defer-with-router-tests.yml b/.github/workflows/defer-with-router-tests.yml index ae816371c8f..e145fecc19a 100644 --- a/.github/workflows/defer-with-router-tests.yml +++ b/.github/workflows/defer-with-router-tests.yml @@ -42,3 +42,31 @@ jobs: DEFER_WITH_ROUTER_TESTS: true run: | ./gradlew --no-daemon --console plain -p tests :defer:allTests + defer-with-apollo-server-tests: + runs-on: ubuntu-latest + if: github.repository == 'apollographql/apollo-kotlin' + steps: + - name: Checkout project + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 + + - name: Install and run graph + working-directory: tests/defer/apollo-server/ + run: | + npm install --legacy-peer-deps + npx patch-package + APOLLO_PORT=4000 npm start & + + - name: Setup Java + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 #v4.2.1 + with: + distribution: 'temurin' + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2 + + - name: Run Apollo Kotlin @defer tests + env: + DEFER_WITH_APOLLO_SERVER_TESTS: true + run: | + ./gradlew --no-daemon --console plain -p tests :defer:allTests diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index 2654939090c..4fffa343371 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -426,6 +426,7 @@ public final class com/apollographql/apollo/api/CustomScalarAdapters$Builder { public final fun deferredFragmentIdentifiers (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun errors (Ljava/util/List;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun falseVariables (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; + public final fun pendingResultIds (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; } public final class com/apollographql/apollo/api/CustomScalarAdapters$Key : com/apollographql/apollo/api/ExecutionContext$Key { diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index 15357f92a74..ff12ab2dbf2 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -938,6 +938,7 @@ final class com.apollographql.apollo.api/CustomScalarAdapters : com.apollographq final fun deferredFragmentIdentifiers(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.deferredFragmentIdentifiers|deferredFragmentIdentifiers(kotlin.collections.Set?){}[0] final fun errors(kotlin.collections/List?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.errors|errors(kotlin.collections.List?){}[0] final fun falseVariables(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.falseVariables|falseVariables(kotlin.collections.Set?){}[0] + final fun pendingResultIds(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.pendingResultIds|pendingResultIds(kotlin.collections.Set?){}[0] } final object Key : com.apollographql.apollo.api/ExecutionContext.Key { // com.apollographql.apollo.api/CustomScalarAdapters.Key|null[0] diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt index 464b4833559..cfe2db00c53 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt @@ -50,7 +50,8 @@ fun and(vararg other: BooleanExpression): BooleanExpression = Bo fun not(other: BooleanExpression): BooleanExpression = BooleanExpression.Not(other) fun variable(name: String): BooleanExpression = BooleanExpression.Element(BVariable(name)) fun label(label: String? = null): BooleanExpression = BooleanExpression.Element(BLabel(label)) -fun possibleTypes(vararg typenames: String): BooleanExpression = BooleanExpression.Element(BPossibleTypes(typenames.toSet())) +fun possibleTypes(vararg typenames: String): BooleanExpression = + BooleanExpression.Element(BPossibleTypes(typenames.toSet())) internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boolean { return when (this) { @@ -66,7 +67,7 @@ internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boo fun BooleanExpression.evaluate( variables: Set?, typename: String?, - deferredFragmentIdentifiers: Set?, + pendingResultIds: Set?, path: List?, ): Boolean { // Remove "data" from the path @@ -74,18 +75,22 @@ fun BooleanExpression.evaluate( return evaluate { when (it) { is BVariable -> !(variables?.contains(it.name) ?: false) - is BLabel -> hasDeferredFragment(deferredFragmentIdentifiers, croppedPath!!, it.label) + is BLabel -> !isDeferredFragmentPending(pendingResultIds, croppedPath!!, it.label) is BPossibleTypes -> it.possibleTypes.contains(typename) } } } -private fun hasDeferredFragment(deferredFragmentIdentifiers: Set?, path: List, label: String?): Boolean { - if (deferredFragmentIdentifiers == null) { +private fun isDeferredFragmentPending( + pendingResultIds: Set?, + path: List, + label: String?, +): Boolean { + if (pendingResultIds == null) { // By default, parse all deferred fragments - this is the case when parsing from the normalized cache. - return true + return false } - return deferredFragmentIdentifiers.contains(DeferredFragmentIdentifier(path, label)) + return pendingResultIds.contains(IncrementalResultIdentifier(path, label)) } /** diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt index a02faf1ac44..585aa94a4c3 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt @@ -19,10 +19,10 @@ class CustomScalarAdapters private constructor( @JvmField val falseVariables: Set?, /** - * Defer identifiers used to determine whether the parser must parse @defer fragments + * Pending incremental result identifiers used to determine whether the parser must parse deferred fragments */ @JvmField - val deferredFragmentIdentifiers: Set?, + val deferredFragmentIdentifiers: Set?, /** * Errors to use with @catch */ @@ -125,21 +125,26 @@ class CustomScalarAdapters private constructor( fun newBuilder(): Builder { return Builder().addAll(this) .falseVariables(falseVariables) - .deferredFragmentIdentifiers(deferredFragmentIdentifiers) + .pendingResultIds(deferredFragmentIdentifiers) } class Builder { private val adaptersMap: MutableMap> = mutableMapOf() private var falseVariables: Set? = null - private var deferredFragmentIdentifiers: Set? = null + private var pendingResultIds: Set? = null private var errors: List? = null fun falseVariables(falseVariables: Set?) = apply { this.falseVariables = falseVariables } - fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { - this.deferredFragmentIdentifiers = deferredFragmentIdentifiers + @Deprecated("Use pendingResultIds instead", ReplaceWith("pendingResultIds(pendingResultIds = deferredFragmentIdentifiers)")) + fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { + this.pendingResultIds = deferredFragmentIdentifiers + } + + fun pendingResultIds(pendingResultIds: Set?) = apply { + this.pendingResultIds = pendingResultIds } fun errors(errors: List?) = apply { @@ -173,7 +178,7 @@ class CustomScalarAdapters private constructor( return CustomScalarAdapters( adaptersMap, falseVariables, - deferredFragmentIdentifiers, + pendingResultIds, errors, ) } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt index 28d0fe1c090..59fda6ca3ef 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt @@ -71,12 +71,12 @@ fun Executable.parseData( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, falseVariables: Set? = null, - deferredFragmentIds: Set? = null, - errors: List? = null + deferredFragmentIds: Set? = null, + errors: List? = null, ): D? { val customScalarAdapters1 = customScalarAdapters.newBuilder() .falseVariables(falseVariables) - .deferredFragmentIdentifiers(deferredFragmentIds) + .pendingResultIds(pendingResultIds = deferredFragmentIds) .errors(errors) .build() return adapter().nullable().fromJson(jsonReader, customScalarAdapters1) @@ -89,4 +89,4 @@ fun Executable.composeData( value: D ) { adapter().toJson(jsonWriter, customScalarAdapters, value) -} \ No newline at end of file +} diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt similarity index 54% rename from libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt rename to libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt index 13f5e2d4dac..0d3d6ec5d6a 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt @@ -7,3 +7,9 @@ data class DeferredFragmentIdentifier( val path: List, val label: String?, ) + +/** + * Identifies an incremental result. + * [DeferredFragmentIdentifier] is kept to not break the API/ABI, but this alias is more descriptive of its purpose. + */ +typealias IncrementalResultIdentifier = DeferredFragmentIdentifier diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt index 9dc586f8147..a97159aa4a0 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt @@ -70,7 +70,7 @@ fun Operation.composeJsonRequest( fun Operation.parseJsonResponse( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return jsonReader.use { ResponseParser.parse( @@ -103,7 +103,7 @@ fun Operation.parseResponse( jsonReader: JsonReader, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( @@ -177,7 +177,7 @@ fun JsonReader.toApolloResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return use { try { @@ -213,7 +213,7 @@ fun JsonReader.parseResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt index 2ce1c62f70f..fc611c83e17 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt @@ -3,8 +3,8 @@ package com.apollographql.apollo.api.internal import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters -import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.falseVariables import com.apollographql.apollo.api.json.JsonReader @@ -24,7 +24,7 @@ internal object ResponseParser { operation: Operation, requestUuid: Uuid?, customScalarAdapters: CustomScalarAdapters, - deferredFragmentIds: Set?, + pendingResultIds: Set?, ): ApolloResponse { jsonReader.beginObject() @@ -36,8 +36,9 @@ internal object ResponseParser { when (val name = jsonReader.nextName()) { "data" -> { val falseVariables = operation.falseVariables(customScalarAdapters) - data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, deferredFragmentIds, errors) + data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, pendingResultIds, errors) } + "errors" -> errors = jsonReader.readErrors() "extensions" -> extensions = jsonReader.readAny() as? Map else -> { @@ -100,7 +101,8 @@ private fun JsonReader.readError(): Error { @Suppress("DEPRECATION") - return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields).build() + return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields) + .build() } private fun JsonReader.readPath(): List? { @@ -164,4 +166,4 @@ fun JsonReader.readErrors(): List { } endArray() return list -} \ No newline at end of file +} diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 1eb9c3330bb..d4e65147106 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/DeferredJsonMerger { +public final class com/apollographql/apollo/internal/IncrementalResultsMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getMergedFragmentIds ()Ljava/util/Set; - public final fun isEmptyPayload ()Z + public final fun getPendingResultIds ()Ljava/util/Set; + public final fun isEmptyResponse ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; public final fun reset ()V diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index 29a86706313..e82fb4ac562 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -200,22 +200,22 @@ final class com.apollographql.apollo.interceptor/AutoPersistedQueryInterceptor : } } -final class com.apollographql.apollo.internal/DeferredJsonMerger { // com.apollographql.apollo.internal/DeferredJsonMerger|null[0] - constructor () // com.apollographql.apollo.internal/DeferredJsonMerger.|(){}[0] - - final val merged // com.apollographql.apollo.internal/DeferredJsonMerger.merged|{}merged[0] - final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merged.|(){}[0] - final val mergedFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds|{}mergedFragmentIds[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds.|(){}[0] - - final var hasNext // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext|{}hasNext[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext.|(){}[0] - final var isEmptyPayload // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload|{}isEmptyPayload[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload.|(){}[0] - - final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(kotlin.collections.Map){}[0] - final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(okio.BufferedSource){}[0] - final fun reset() // com.apollographql.apollo.internal/DeferredJsonMerger.reset|reset(){}[0] +final class com.apollographql.apollo.internal/IncrementalResultsMerger { // com.apollographql.apollo.internal/IncrementalResultsMerger|null[0] + constructor () // com.apollographql.apollo.internal/IncrementalResultsMerger.|(){}[0] + + final val merged // com.apollographql.apollo.internal/IncrementalResultsMerger.merged|{}merged[0] + final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merged.|(){}[0] + final val pendingResultIds // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds|{}pendingResultIds[0] + final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds.|(){}[0] + + final var hasNext // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext|{}hasNext[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext.|(){}[0] + final var isEmptyResponse // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse.|(){}[0] + + final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] + final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] + final fun reset() // com.apollographql.apollo.internal/IncrementalResultsMerger.reset|reset(){}[0] } final class com.apollographql.apollo.internal/MultipartReader : okio/Closeable { // com.apollographql.apollo.internal/MultipartReader|null[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index ae89653f943..8806ffc7efd 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/DeferredJsonMerger { +public final class com/apollographql/apollo/internal/IncrementalResultsMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getMergedFragmentIds ()Ljava/util/Set; - public final fun isEmptyPayload ()Z + public final fun getPendingResultIds ()Ljava/util/Set; + public final fun isEmptyResponse ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; public final fun reset ()V diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt deleted file mode 100644 index b034caecc28..00000000000 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.apollographql.apollo.internal - -import com.apollographql.apollo.annotations.ApolloInternal -import com.apollographql.apollo.api.DeferredFragmentIdentifier -import com.apollographql.apollo.api.json.BufferedSourceJsonReader -import com.apollographql.apollo.api.json.readAny -import okio.BufferedSource - -private typealias JsonMap = Map -private typealias MutableJsonMap = MutableMap - -/** - * Utility class for merging GraphQL JSON payloads received in multiple chunks when using the `@defer` directive. - * - * Each call to [merge] will merge the given chunk into the [merged] Map, and will also update the [mergedFragmentIds] Set with the - * value of its `path` and `label` field. - * - * The fields in `data` are merged into the node found in [merged] at `path` (for the first call to [merge], the payload is - * copied to [merged] as-is). - * - * `errors` in incremental items (if present) are merged together in an array and then set to the `errors` field of the [merged] Map, - * at each call to [merge]. - * `extensions` in incremental items (if present) are merged together in an array and then set to the `extensions/incremental` field of the - * [merged] Map, at each call to [merge]. - */ -@ApolloInternal -class DeferredJsonMerger { - private val _merged: MutableJsonMap = mutableMapOf() - val merged: JsonMap = _merged - - private val _mergedFragmentIds = mutableSetOf() - val mergedFragmentIds: Set = _mergedFragmentIds - - var hasNext: Boolean = true - private set - - /** - * A payload can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last - * emitted payload. This field allows to test for this in order to ignore such payloads. - * See https://github.com/apollographql/router/issues/1687. - */ - var isEmptyPayload: Boolean = false - private set - - fun merge(payload: BufferedSource): JsonMap { - val payloadMap = jsonToMap(payload) - return merge(payloadMap) - } - - @Suppress("UNCHECKED_CAST") - fun merge(payload: JsonMap): JsonMap { - if (merged.isEmpty()) { - // Initial payload, no merging needed - _merged += payload - return merged - } - - val incrementalList = payload["incremental"] as? List - if (incrementalList == null) { - isEmptyPayload = true - } else { - isEmptyPayload = false - val mergedErrors = mutableListOf() - val mergedExtensions = mutableListOf() - for (incrementalItem in incrementalList) { - mergeData(incrementalItem) - // Merge errors and extensions (if any) of the incremental list - (incrementalItem["errors"] as? List)?.let { mergedErrors += it } - (incrementalItem["extensions"] as? JsonMap)?.let { mergedExtensions += it } - } - // Keep only this payload's errors and extensions, if any - if (mergedErrors.isNotEmpty()) { - _merged["errors"] = mergedErrors - } else { - _merged.remove("errors") - } - if (mergedExtensions.isNotEmpty()) { - _merged["extensions"] = mapOf("incremental" to mergedExtensions) - } else { - _merged.remove("extensions") - } - } - - hasNext = payload["hasNext"] as Boolean? ?: false - - return merged - } - - @Suppress("UNCHECKED_CAST") - private fun mergeData(incrementalItem: JsonMap) { - val data = incrementalItem["data"] as JsonMap? - val path = incrementalItem["path"] as List - val mergedData = merged["data"] as JsonMap - - // payloadData can be null if there are errors - if (data != null) { - val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap - deepMerge(nodeToMergeInto, data) - - _mergedFragmentIds += DeferredFragmentIdentifier(path = path, label = incrementalItem["label"] as String?) - } - } - - @Suppress("UNCHECKED_CAST") - private fun deepMerge(destination: MutableJsonMap, map: JsonMap) { - for ((key, value) in map) { - if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { - // Objects: merge recursively - val fieldDestination = destination[key] as MutableJsonMap - val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") - deepMerge(destination = fieldDestination, map = fieldMap) - } else { - // Other types: add / overwrite - destination[key] = value - } - } - } - - @Suppress("UNCHECKED_CAST") - private fun jsonToMap(json: BufferedSource): JsonMap = BufferedSourceJsonReader(json).readAny() as JsonMap - - - /** - * Find the node in the [map] at the given [path]. - * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). - */ - private fun nodeAtPath(map: JsonMap, path: List): Any? { - var node: Any? = map - for (key in path) { - node = if (node is List<*>) { - node[key as Int] - } else { - @Suppress("UNCHECKED_CAST") - node as JsonMap - node[key] - } - } - return node - } - - fun reset() { - _merged.clear() - _mergedFragmentIds.clear() - hasNext = true - isEmptyPayload = false - } -} - -internal fun JsonMap.isDeferred(): Boolean { - return keys.contains("hasNext") -} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt new file mode 100644 index 00000000000..b49e1af3dd0 --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt @@ -0,0 +1,186 @@ +package com.apollographql.apollo.internal + +import com.apollographql.apollo.annotations.ApolloInternal +import com.apollographql.apollo.api.IncrementalResultIdentifier +import com.apollographql.apollo.api.json.BufferedSourceJsonReader +import com.apollographql.apollo.api.json.readAny +import okio.BufferedSource + +private typealias JsonMap = Map +private typealias MutableJsonMap = MutableMap + +/** + * Utility class for merging GraphQL incremental results received in multiple chunks when using the `@defer` and/or `@stream` directives. + * + * Each call to [merge] will merge the given results into the [merged] Map, and will also update the [pendingResultIds] Set with the + * value of their `path` and `label` fields. + * + * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field. For the first call to + * [merge], the payload is copied to [merged] as-is. + * + * `errors` in incremental and completed results (if present) are merged together in an array and then set to the `errors` field of the + * [merged] Map. + * `extensions` in incremental results (if present) are merged together in an array and then set to the `extensions` field of the [merged] + * Map. + */ +@ApolloInternal +@Suppress("UNCHECKED_CAST") +class IncrementalResultsMerger { + private val _merged: MutableJsonMap = mutableMapOf() + val merged: JsonMap = _merged + + /** + * Map of identifiers to their corresponding IncrementalResultIdentifier, found in `pending`. + */ + private val _pendingResultIds = mutableMapOf() + val pendingResultIds: Set get() = _pendingResultIds.values.toSet() + + var hasNext: Boolean = true + private set + + /** + * A response can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last + * emitted payload. This field allows to test for this in order to ignore such payloads. + * See https://github.com/apollographql/router/issues/1687. + */ + var isEmptyResponse: Boolean = false + private set + + fun merge(part: BufferedSource): JsonMap { + return merge(part.toJsonMap()) + } + + fun merge(part: JsonMap): JsonMap { + val completed = part["completed"] as? List + if (merged.isEmpty()) { + // Initial part, no merging needed (strip some fields that should not appear in the final result) + _merged += part - "hasNext" - "pending" + handlePending(part) + handleCompleted(completed) + return merged + } + handlePending(part) + + val incremental = part["incremental"] as? List + if (incremental != null) { + for (incrementalResult in incremental) { + mergeIncrementalResult(incrementalResult) + // Merge errors (if any) of the incremental result + (incrementalResult["errors"] as? List)?.let { getOrPutMergedErrors() += it } + } + } + isEmptyResponse = completed == null && incremental == null + + hasNext = part["hasNext"] as Boolean? ?: false + + handleCompleted(completed) + + (part["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } + + return merged + } + + private fun getOrPutMergedErrors() = _merged.getOrPut("errors") { mutableListOf() } as MutableList + + private fun getOrPutExtensions() = _merged.getOrPut("extensions") { mutableMapOf() } as MutableJsonMap + + private fun handlePending(part: JsonMap) { + val pending = part["pending"] as? List + if (pending != null) { + for (pendingResult in pending) { + val id = pendingResult["id"] as String + val path = pendingResult["path"] as List + val label = pendingResult["label"] as String? + _pendingResultIds[id] = IncrementalResultIdentifier(path = path, label = label) + } + } + } + + private fun handleCompleted(completed: List?) { + if (completed != null) { + for (completedResult in completed) { + // Merge errors (if any) of the completed result + val errors = completedResult["errors"] as? List + if (errors != null) { + getOrPutMergedErrors() += errors + } else { + // Fragment is no longer pending - only if there were no errors + val id = completedResult["id"] as String + _pendingResultIds.remove(id) ?: error("Id '$id' not found in pending results") + } + } + } + } + + private fun mergeIncrementalResult(incrementalResult: JsonMap) { + val id = incrementalResult["id"] as String? ?: error("No id found in incremental result") + val data = incrementalResult["data"] as JsonMap? + val items = incrementalResult["items"] as List? + val subPath = incrementalResult["subPath"] as List? ?: emptyList() + val path = (_pendingResultIds[id]?.path ?: error("Id '$id' not found in pending results")) + subPath + val mergedData = merged["data"] as JsonMap + val nodeToMergeInto = nodeAtPath(mergedData, path) + when { + data != null -> { + deepMergeObject(nodeToMergeInto as MutableJsonMap, data) + } + + items != null -> { + mergeList(nodeToMergeInto as MutableList, items) + } + + else -> { + error("Neither data nor items found in incremental result") + } + } + } + + private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { + for ((key, value) in obj) { + if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { + // Objects: merge recursively + val fieldDestination = destination[key] as MutableJsonMap + val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") + deepMergeObject(destination = fieldDestination, obj = fieldMap) + } else { + // Other types: add / overwrite + destination[key] = value + } + } + } + + private fun mergeList(destination: MutableList, items: List) { + destination.addAll(items) + } + + private fun BufferedSource.toJsonMap(): JsonMap = BufferedSourceJsonReader(this).readAny() as JsonMap + + + /** + * Find the node in the [map] at the given [path]. + * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). + */ + private fun nodeAtPath(map: JsonMap, path: List): Any? { + var node: Any? = map + for (key in path) { + node = if (node is List<*>) { + node[key as Int] + } else { + node as JsonMap + node[key] + } + } + return node + } + + fun reset() { + _merged.clear() + _pendingResultIds.clear() + hasNext = true + isEmptyResponse = false + } +} + +internal fun JsonMap.isIncremental(): Boolean { + return keys.contains("hasNext") +} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 5a2e29cc9b8..959f7a6a16d 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -21,7 +21,7 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloHttpException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.RouterError -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import com.apollographql.apollo.internal.isGraphQLResponse import com.apollographql.apollo.internal.isMultipart import com.apollographql.apollo.internal.multipartBodyFlow @@ -170,7 +170,7 @@ private constructor( customScalarAdapters: CustomScalarAdapters, httpResponse: HttpResponse, ): Flow> { - var jsonMerger: DeferredJsonMerger? = null + var incrementalResultsMerger: IncrementalResultsMerger? = null val operation = request.operation return multipartBodyFlow(httpResponse) @@ -218,21 +218,20 @@ private constructor( else -> null } } else { - if (jsonMerger == null) { - jsonMerger = DeferredJsonMerger() + if (incrementalResultsMerger == null) { + incrementalResultsMerger = IncrementalResultsMerger() } - val merged = jsonMerger.merge(part) - val deferredFragmentIds = jsonMerger.mergedFragmentIds - val isLast = !jsonMerger.hasNext + val merged = incrementalResultsMerger.merge(part) + val pendingResultIds = incrementalResultsMerger.pendingResultIds + val isLast = !incrementalResultsMerger.hasNext - if (jsonMerger.isEmptyPayload) { + if (incrementalResultsMerger.isEmptyResponse) { null } else { - @Suppress("DEPRECATION") merged.jsonReader().toApolloResponse( operation = operation, customScalarAdapters = customScalarAdapters, - deferredFragmentIdentifiers = deferredFragmentIds + deferredFragmentIdentifiers = pendingResultIds ).newBuilder().isLast(isLast).build() } } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt index 7dec7222547..e6e2370c49f 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt @@ -12,7 +12,7 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloWebSocketForceCloseException import com.apollographql.apollo.exception.DefaultApolloException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.websocket.internal.OperationListener import com.apollographql.apollo.network.websocket.internal.WebSocketPool @@ -202,7 +202,7 @@ private object DefaultSubscriptionParserFactory: SubscriptionParserFactory { } private class DefaultSubscriptionParser(private val request: ApolloRequest) : SubscriptionParser { - private var deferredJsonMerger: DeferredJsonMerger = DeferredJsonMerger() + private var incrementalResultsMerger: IncrementalResultsMerger = IncrementalResultsMerger() private val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty @Suppress("NAME_SHADOWING") @@ -215,7 +215,7 @@ private class DefaultSubscriptionParser(private val request: } val (payload, mergedFragmentIds) = if (responseMap.isDeferred()) { - deferredJsonMerger.merge(responseMap) to deferredJsonMerger.mergedFragmentIds + incrementalResultsMerger.merge(responseMap) to incrementalResultsMerger.pendingResultIds } else { responseMap to null } @@ -226,12 +226,12 @@ private class DefaultSubscriptionParser(private val request: deferredFragmentIdentifiers = mergedFragmentIds ) - if (!deferredJsonMerger.hasNext) { - // Last deferred payload: reset the deferredJsonMerger for potential subsequent responses - deferredJsonMerger.reset() + if (!incrementalResultsMerger.hasNext) { + // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() } - return if (deferredJsonMerger.isEmptyPayload) { + return if (incrementalResultsMerger.isEmptyResponse) { null } else { apolloResponse diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt index 80383932fcc..549638ed687 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt @@ -10,8 +10,8 @@ import com.apollographql.apollo.api.toApolloResponse import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.DeferredJsonMerger -import com.apollographql.apollo.internal.isDeferred +import com.apollographql.apollo.internal.IncrementalResultsMerger +import com.apollographql.apollo.internal.isIncremental import com.apollographql.apollo.internal.transformWhile import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.ws.internal.Command @@ -45,7 +45,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.launch -import okio.use /** * A [NetworkTransport] that manages a single instance of a [WebSocketConnection]. @@ -263,7 +262,7 @@ private constructor( override fun execute( request: ApolloRequest, ): Flow> { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() return events.onSubscription { messages.send(StartOperation(request)) @@ -303,8 +302,8 @@ private constructor( is OperationResponse -> { val responsePayload = response.payload val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters]!! - val (payload, mergedFragmentIds) = if (responsePayload.isDeferred()) { - deferredJsonMerger.merge(responsePayload) to deferredJsonMerger.mergedFragmentIds + val (payload, mergedFragmentIds) = if (responsePayload.isIncremental()) { + incrementalResultsMerger.merge(responsePayload) to incrementalResultsMerger.pendingResultIds } else { responsePayload to null } @@ -315,9 +314,9 @@ private constructor( deferredFragmentIdentifiers = mergedFragmentIds ) - if (!deferredJsonMerger.hasNext) { - // Last deferred payload: reset the deferredJsonMerger for potential subsequent responses - deferredJsonMerger.reset() + if (!incrementalResultsMerger.hasNext) { + // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() } apolloResponse } @@ -329,7 +328,7 @@ private constructor( is ConnectionReEstablished, is OperationComplete, is GeneralError -> error("Unexpected event $response") } }.filterNot { - deferredJsonMerger.isEmptyPayload + incrementalResultsMerger.isEmptyResponse }.onCompletion { messages.send(StopOperation(request)) } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt deleted file mode 100644 index 40918461ae3..00000000000 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ /dev/null @@ -1,713 +0,0 @@ -package test.defer - -import com.apollographql.apollo.api.DeferredFragmentIdentifier -import com.apollographql.apollo.api.json.BufferedSourceJsonReader -import com.apollographql.apollo.api.json.readAny -import com.apollographql.apollo.internal.DeferredJsonMerger -import okio.Buffer -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -private fun String.buffer() = Buffer().writeUtf8(this) - -@Suppress("UNCHECKED_CAST") -private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map - -class DeferredJsonMergerTest { - @Test - fun mergeJsonSingleIncrementalItem() { - val deferredJsonMerger = DeferredJsonMerger() - - val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "hasNext": true - } - """ - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(payload1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) - - - val payload2 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - } - } - ], - "hasNext": true - } - """ - val mergedPayloads_1_2 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "duration": { - "amount": 100, - "unit": "ms" - } - } - ] - } - } - """ - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - - - val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } - } - } - ], - "hasNext": true - } - """ - val mergedPayloads_1_2_3 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } - } - ] - }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "duration": { - "amount": 25, - "unit": "ms" - } - } - ] - } - } - """ - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - - - val payload4 = """ - { - "incremental": [ - { - "data": null, - "path": [ - "computers", - 0, - "screen" - ], - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], - "label": "fragment:ComputerFields:0" - } - ], - "hasNext": true - } - """ - val mergedPayloads_1_2_3_4 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } - } - ] - }, - "hasNext": true, - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ] - } - """ - deferredJsonMerger.merge(payload4.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - - - val payload5 = """ - { - "incremental": [ - { - "data": { - "isColor": false - }, - "path": [ - "computers", - 1, - "screen" - ], - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ], - "label": "fragment:ComputerFields:0", - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - } - } - ], - "hasNext": false - } - """ - val mergedPayloads_1_2_3_4_5 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480", - "isColor": false - } - } - ] - }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - } - ] - }, - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ] - } - """ - deferredJsonMerger.merge(payload5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - } - - @Test - fun mergeJsonMultipleIncrementalItems() { - val deferredJsonMerger = DeferredJsonMerger() - - val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "hasNext": true - } - """ - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(payload1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) - - - val payload2_3 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - } - }, - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } - } - } - ], - "hasNext": true - } - """ - val mergedPayloads_1_2_3 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } - } - ] - }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "duration": { - "amount": 100, - "unit": "ms" - } - }, - { - "duration": { - "amount": 25, - "unit": "ms" - } - } - ] - } - } - """ - deferredJsonMerger.merge(payload2_3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - - - val payload4_5 = """ - { - "incremental": [ - { - "data": null, - "path": [ - "computers", - 0, - "screen" - ], - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], - "label": "fragment:ComputerFields:0" - }, - { - "data": { - "isColor": false - }, - "path": [ - "computers", - 1, - "screen" - ], - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ], - "label": "fragment:ComputerFields:0", - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - } - } - ], - "hasNext": true - } - """ - val mergedPayloads_1_2_3_4_5 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480", - "isColor": false - } - } - ] - }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - } - ] - }, - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - }, - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ] - } - """ - deferredJsonMerger.merge(payload4_5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - } - - @Test - fun emptyPayloads() { - val deferredJsonMerger = DeferredJsonMerger() - - val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "hasNext": true - } - """ - deferredJsonMerger.merge(payload1.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) - - val payload2 = """ - { - "hasNext": true - } - """ - deferredJsonMerger.merge(payload2.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) - - val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - } - } - ], - "hasNext": true - } - """ - deferredJsonMerger.merge(payload3.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) - - val payload4 = """ - { - "hasNext": false - } - """ - deferredJsonMerger.merge(payload4.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) - } -} \ No newline at end of file diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt new file mode 100644 index 00000000000..e743fa6f2ac --- /dev/null +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt @@ -0,0 +1,2509 @@ +@file:OptIn(ApolloInternal::class) + +package test.defer + +import com.apollographql.apollo.annotations.ApolloInternal +import com.apollographql.apollo.api.IncrementalResultIdentifier +import com.apollographql.apollo.api.json.BufferedSourceJsonReader +import com.apollographql.apollo.api.json.readAny +import com.apollographql.apollo.internal.IncrementalResultsMerger +import okio.Buffer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +private fun String.buffer() = Buffer().writeUtf8(this) + +@Suppress("UNCHECKED_CAST") +private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map + +class IncrementalResultsMergerTest { + @Test + fun mergeJsonSingleIncrementalItem() { + val incrementalResultsMerger = IncrementalResultsMerger() + + //language=JSON + val payload1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + } + ], + "completed": [ + { + "id": "0" + } + ], + "pending": [ + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0") + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } + }, + "id": "1" + } + ], + "completed": [ + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } + } + ] + }, + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload4 = """ + { + "completed": [ + { + "id": "2", + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ] + } + ], + "pending": [ + { + "id": "3", + "path": [ + "computers", + 1, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } + } + ] + }, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload5 = """ + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ] + } + ], + "completed": [ + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } + } + ] + }, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + }, + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + ), + incrementalResultsMerger.pendingResultIds + ) + } + + @Test + fun mergeJsonMultipleIncrementalItems() { + val incrementalResultsMerger = IncrementalResultsMerger() + + //language=JSON + val payload1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2_3 = """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + }, + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } + }, + "id": "1" + } + ], + "completed": [ + { + "id": "0" + }, + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + }, + { + "id": "3", + "path": [ + "computers", + 1, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } + } + ] + }, + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2_3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload4_5 = """ + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ] + } + ], + "completed": [ + { + "id": "2", + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ] + }, + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } + } + ] + }, + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + }, + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload4_5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + ), + incrementalResultsMerger.pendingResultIds + ) + } + + @Test + fun emptyPayloads() { + val incrementalResultsMerger = IncrementalResultsMerger() + + //language=JSON + val payload1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) + + //language=JSON + val payload2 = """ + { + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) + + //language=JSON + val payload4 = """ + { + "hasNext": false + } + """.trimIndent() + incrementalResultsMerger.merge(payload4.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) + } + + /** + * Example A from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleA() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i" + } + } + } + }, + "pending": [ + { + "path": [], + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i" + } + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = null), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "MyFragment": "Query" + } + }, + { + "id": "0", + "subPath": [ + "f2", + "c", + "f" + ], + "data": { + "j": "j" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i", + "j": "j" + } + } + }, + "MyFragment": "Query" + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example A2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleA2() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" + } + } + } + }, + "pending": [ + { + "id": "0", + "path": [], + "label": "D1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" + } + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "D1"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "subPath": [ + "f2", + "c", + "f" + ], + "data": { + "j": "J", + "k": "K" + } + } + ], + "pending": [ + { + "id": "1", + "path": [ + "f2", + "c", + "f" + ], + "label": "D2" + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K" + } + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("f2", "c", "f"), label = "D2"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "id": "1", + "data": { + "l": "L", + "m": "M" + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } + """.trimIndent() + + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K", + "l": "L", + "m": "M" + } + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example B1 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleB1() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + }, + "pending": [ + { + "path": [], + "id": "0", + "label": "Blue" + }, + { + "path": [ + "a", + "b" + ], + "id": "1", + "label": "Red" + } + ], + "hasNext": true + } + """.trimIndent() + + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "data": { + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + { + "id": "1", + "data": { + "e": { + "f": "f" + } + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example B2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleB2() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + }, + "pending": [ + { + "path": [], + "id": "0", + "label": "Blue" + }, + { + "path": [ + "a", + "b" + ], + "id": "1", + "label": "Red" + } + ], + "hasNext": true + } + """.trimIndent() + + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + }, + { + "id": "1", + "data": { + "e": { + "f": "f" + } + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + } + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "id": "1", + "data": { + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } + """.trimIndent() + val mergedPayloads_1_2_3 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """ + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example D from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleD() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": {} + }, + "pending": [ + { + "path": [], + "id": "0" + }, + { + "path": [ + "me" + ], + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf("me"), label = null), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "data": { + "list": [ + { + "item": {} + }, + { + "item": {} + }, + { + "item": {} + } + ] + } + }, + { + "id": "1", + "subPath": [ + "list", + 0, + "item" + ], + "data": { + "id": "1" + } + }, + { + "id": "1", + "subPath": [ + "list", + 1, + "item" + ], + "data": { + "id": "2" + } + }, + { + "id": "1", + "subPath": [ + "list", + 2, + "item" + ], + "data": { + "id": "3" + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "list": [ + { + "item": { + "id": "1" + } + }, + { + "item": { + "id": "2" + } + }, + { + "item": { + "id": "3" + } + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = null), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "id": "0", + "subPath": [ + "me", + "list", + 0, + "item" + ], + "data": { + "value": "Foo" + } + }, + { + "id": "0", + "subPath": [ + "me", + "list", + 1, + "item" + ], + "data": { + "value": "Bar" + } + }, + { + "id": "0", + "subPath": [ + "me", + "list", + 2, + "item" + ], + "data": { + "value": "Baz" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "me": { + "list": [ + { + "item": { + "id": "1", + "value": "Foo" + } + }, + { + "item": { + "id": "2", + "value": "Bar" + } + }, + { + "item": { + "id": "3", + "value": "Baz" + } + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example F from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleF() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": {} + }, + "pending": [ + { + "id": "0", + "path": [ + "me" + ], + "label": "B" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "B"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "a": "A", + "b": "B" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "a": "A", + "b": "B" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example G from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleG() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ] + } + }, + "pending": [ + { + "id": "0", + "path": [ + "me" + ], + "label": "Billing" + }, + { + "id": "1", + "path": [ + "me" + ], + "label": "Prev" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "Billing"), + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34" + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "id": "1", + "data": { + "previousInvoices": [ + { + "name": "My Invoice" + } + ] + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34", + "previousInvoices": [ + { + "name": "My Invoice" + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example H from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleH() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": {} + }, + "pending": [ + { + "id": "0", + "path": [], + "label": "A" + }, + { + "id": "1", + "path": [ + "me" + ], + "label": "B" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "A"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "subPath": [ + "me" + ], + "data": { + "foo": { + "bar": {} + } + } + }, + { + "id": "0", + "subPath": [ + "me", + "foo", + "bar" + ], + "data": { + "baz": "BAZ" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" + } + } + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "B"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1", + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": [ + "foo", + "bar", + "qux" + ] + } + ] + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" + } + } + } + }, + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": [ + "foo", + "bar", + "qux" + ] + } + ] + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "B"), + ), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example I from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleI() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + }, + "pending": [ + { + "id": "0", + "path": [ + "person" + ], + "label": "homeWorldDefer" + }, + { + "id": "1", + "path": [ + "person", + "films" + ], + "label": "filmsStream" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "items": [ + { + "title": "Return of the Jedi" + } + ] + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } + } + } + """.trimIndent() + + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload4 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "homeworld": { + "name": "Tatooine" + } + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "homeworld": { + "name": "Tatooine" + }, + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } + } + } + """.trimIndent() + + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.pendingResultIds + ) + } + + /** + * Example J from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleJ() { + val incrementalResultsMerger = IncrementalResultsMerger() + //language=JSON + val payload1 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + } + ] + } + }, + "pending": [ + { + "id": "1", + "path": [ + "person", + "films" + ], + "label": "filmsStream" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "items": [ + { + "title": "The Empire Strikes Back" + } + ] + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.pendingResultIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1", + "errors": [ + { + "message": "Cannot return null for non-nullable field Person.films.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "person", + "films" + ] + } + ] + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + }, + "errors": [ + { + "message": "Cannot return null for non-nullable field Person.films.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "person", + "films" + ] + } + ] + } + """.trimIndent() + + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.pendingResultIds + ) + } +} diff --git a/tests/defer/README.md b/tests/defer/README.md index 9e4ad207f3d..4f4ac085620 100644 --- a/tests/defer/README.md +++ b/tests/defer/README.md @@ -16,3 +16,16 @@ To run them locally: subgraph: `(cd tests/defer/router/subgraphs/computers && npm install && APOLLO_PORT=4001 npm start)&` 2. Run the router: `path/to/router --supergraph tests/defer/router/simple-supergraph.graphqls &` 3. Run the tests: `DEFER_WITH_ROUTER_TESTS=true ./gradlew -p tests :defer:allTests` + +## End-to-end tests with Apollo Server + +The tests in `DeferWithApolloServerTest` are not run by default (they are excluded in the gradle conf) because they +expect an instance of [Apollo Server](https://www.apollographql.com/docs/apollo-server) running locally. + +They are enabled only when running from the specific `defer-with-apollo-server-tests` CI workflow. + +To run them locally: + +1. Install and run the + subgraph: `(cd tests/defer/apollo-server && npm install --legacy-peer-deps && npx patch-package && APOLLO_PORT=4000 npm start)&` +2. Run the tests: `DEFER_WITH_APOLLO_SERVER_TESTS=true ./gradlew -p tests :defer:allTests` diff --git a/tests/defer/apollo-server/README.md b/tests/defer/apollo-server/README.md new file mode 100644 index 00000000000..ef149563b19 --- /dev/null +++ b/tests/defer/apollo-server/README.md @@ -0,0 +1,4 @@ +# Test server using Apollo Server, for `@defer` tests + +- This uses graphql-js `17.0.0-alpha.7`, which implements the latest draft of the `@defer` incremental format (as of 2024-12-16). +- Apollo Server `4.11.2` needs a patch (in `patches`) to surface this format in the responses. diff --git a/tests/defer/apollo-server/computers.graphqls b/tests/defer/apollo-server/computers.graphqls new file mode 100644 index 00000000000..a1875342b39 --- /dev/null +++ b/tests/defer/apollo-server/computers.graphqls @@ -0,0 +1,34 @@ +type Query { + computers: [Computer!]! + computer(id: ID!): Computer +} + +type Mutation { + computers: [Computer!]! +} + +type Computer { + id: ID! + cpu: String! + year: Int! + screen: Screen! + errorField: String + nonNullErrorField: String! + peripherals: [String!]! +} + +type Screen { + resolution: String! + isColor: Boolean! +} + +directive @defer( + if: Boolean! = true + label: String +) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD diff --git a/tests/defer/apollo-server/computers.js b/tests/defer/apollo-server/computers.js new file mode 100644 index 00000000000..77e673b7985 --- /dev/null +++ b/tests/defer/apollo-server/computers.js @@ -0,0 +1,52 @@ +import {ApolloServer} from '@apollo/server'; +import {startStandaloneServer} from '@apollo/server/standalone'; +import {readFileSync} from 'fs'; + +const port = process.env.APOLLO_PORT || 4000; + +const computers = [ + { + id: 'Computer1', + cpu: "386", + year: 1993, + screen: {resolution: "640x480", isColor: false}, + peripherals: ["Keyboard", "Mouse", "Printer"], + }, + { + id: 'Computer2', + cpu: "486", + year: 1996, + screen: {resolution: "800x600", isColor: true}, + peripherals: ["Keyboard", "Mouse", "Printer", "Scanner"], + }, +] + +const typeDefs = readFileSync('./computers.graphqls', {encoding: 'utf-8'}); +const resolvers = { + Query: { + computers: (_, args, context) => { + return computers; + }, + computer: (_, args, context) => { + return computers.find(p => p.id === args.id); + } + }, + Mutation: { + computers: (_, args, context) => { + return computers; + } + }, + Computer: { + errorField: (_, args, context) => { + throw new Error("Error field"); + }, + nonNullErrorField: (_, args, context) => { + return null; + } + } +} +const server = new ApolloServer({typeDefs, resolvers}); +const {url} = await startStandaloneServer(server, { + listen: {port: port}, +}); +console.log(`🚀 Computers subgraph ready at ${url}`); diff --git a/tests/defer/apollo-server/package.json b/tests/defer/apollo-server/package.json new file mode 100644 index 00000000000..1b469e77c4e --- /dev/null +++ b/tests/defer/apollo-server/package.json @@ -0,0 +1,18 @@ +{ + "type": "module", + "name": "subgraph-computers", + "version": "1.1.0", + "description": "", + "main": "computers.js", + "scripts": { + "start": "node computers.js" + }, + "dependencies": { + "@apollo/server": "4.11.2", + "graphql": "17.0.0-alpha.7", + "patch-package": "^8.0.0" + }, + "keywords": [], + "author": "", + "license": "MIT" +} diff --git a/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch b/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch new file mode 100644 index 00000000000..d6a742855b7 --- /dev/null +++ b/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch @@ -0,0 +1,28 @@ +diff --git a/node_modules/@apollo/server/dist/esm/runHttpQuery.js b/node_modules/@apollo/server/dist/esm/runHttpQuery.js +index 96ef0ab..0d341fa 100644 +--- a/node_modules/@apollo/server/dist/esm/runHttpQuery.js ++++ b/node_modules/@apollo/server/dist/esm/runHttpQuery.js +@@ -187,6 +187,7 @@ function orderExecutionResultFields(result) { + } + function orderInitialIncrementalExecutionResultFields(result) { + return { ++ ...result, + hasNext: result.hasNext, + errors: result.errors, + data: result.data, +@@ -196,6 +197,7 @@ function orderInitialIncrementalExecutionResultFields(result) { + } + function orderSubsequentIncrementalExecutionResultFields(result) { + return { ++ ...result, + hasNext: result.hasNext, + incremental: orderIncrementalResultFields(result.incremental), + extensions: result.extensions, +@@ -203,6 +205,7 @@ function orderSubsequentIncrementalExecutionResultFields(result) { + } + function orderIncrementalResultFields(incremental) { + return incremental?.map((i) => ({ ++ ...i, + hasNext: i.hasNext, + errors: i.errors, + path: i.path, diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index 6448f9c36d0..d90c4f5fd6d 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -50,6 +50,14 @@ fun configureApollo(generateKotlinModels: Boolean) { } } +apollo { + service("noTypename") { + packageName.set("defer.notypename") + srcDir("src/commonMain/graphql/noTypename") + addTypename.set("ifPolymorphic") + } +} + configureApollo(true) if (System.getProperty("idea.sync.active") == null) { registerJavaCodegenTestTask() @@ -67,12 +75,15 @@ fun com.apollographql.apollo.gradle.api.Service.configureConnection(generateKotl } tasks.withType(AbstractTestTask::class.java) { - // Run the defer with Router tests only from a specific CI job + // Run the defer with Router and defer with Apollo Server tests only from a specific CI job val runDeferWithRouterTests = System.getenv("DEFER_WITH_ROUTER_TESTS").toBoolean() - if (runDeferWithRouterTests) { - filter.setIncludePatterns("test.DeferWithRouterTest") - } else { - filter.setExcludePatterns("test.DeferWithRouterTest") - } + val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() + filter.setIncludePatterns(*buildList { + if (runDeferWithRouterTests) add("test.DeferWithRouterTest") + if (runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest") + }.toTypedArray()) + filter.setExcludePatterns(*buildList { + if (!runDeferWithRouterTests) add("test.DeferWithRouterTest") + if (!runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest") + }.toTypedArray()) } - diff --git a/tests/defer/src/commonMain/graphql/base/operation.graphql b/tests/defer/src/commonMain/graphql/base/operation.graphql index fad24d09441..c3b9b8d57b9 100644 --- a/tests/defer/src/commonMain/graphql/base/operation.graphql +++ b/tests/defer/src/commonMain/graphql/base/operation.graphql @@ -109,6 +109,15 @@ query CanDeferAFragmentThatIsAlsoNotDeferredDeferredFragmentIsFirstQuery { } } +query DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query { + computer(id: "Computer1") { + screen { + ...ScreenFields @defer + ...ScreenFields + } + } +} + query CanDeferAFragmentThatIsAlsoNotDeferredNotDeferredFragmentIsFirstQuery { computer(id: "Computer1") { screen { @@ -118,6 +127,15 @@ query CanDeferAFragmentThatIsAlsoNotDeferredNotDeferredFragmentIsFirstQuery { } } +query DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query { + computer(id: "Computer1") { + screen { + ...ScreenFields + ...ScreenFields @defer + } + } +} + query HandlesErrorsThrownInDeferredFragmentsQuery { computer(id: "Computer1") { id @@ -150,3 +168,59 @@ query HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery { fragment ComputerIdField on Computer { id } + +query OverlappingQuery { + computer(id: "Computer1") { + id + ... on Computer @defer(label: "a") { + id + ... on Computer @defer(label: "b") { + id + cpu + year + } + } + } +} + +query Overlapping2Query { + computer(id: "Computer1") { + id + ... on Computer @defer(label: "a") { + id + } + ... on Computer @defer(label: "b") { + id + cpu + year + } + } +} + +query SubPathQuery { + computer(id: "Computer1") { + id + } + ... on Query @defer(label: "a") { + MyFragment: __typename + computer(id: "Computer1") { + id + screen { + isColor + } + } + } +} + +query SimpleStreamQuery($initialCount: Int!) { + computers @stream(initialCount: $initialCount) { + id + } +} + +query NestedStreamQuery($initialCount: Int!) { + computers @stream(initialCount: $initialCount) { + id + peripherals @stream(initialCount: $initialCount) + } +} diff --git a/tests/defer/src/commonMain/graphql/base/schema.graphqls b/tests/defer/src/commonMain/graphql/base/schema.graphqls index 0892f0f4075..9de38fc9a65 100644 --- a/tests/defer/src/commonMain/graphql/base/schema.graphqls +++ b/tests/defer/src/commonMain/graphql/base/schema.graphqls @@ -23,9 +23,16 @@ type Computer { screen: Screen! errorField: String nonNullErrorField: String! + peripherals: [String!]! } type Screen { resolution: String! isColor: Boolean! } + +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD diff --git a/tests/defer/src/commonMain/graphql/noTypename/operation.graphql b/tests/defer/src/commonMain/graphql/noTypename/operation.graphql new file mode 100644 index 00000000000..90ca0b72dda --- /dev/null +++ b/tests/defer/src/commonMain/graphql/noTypename/operation.graphql @@ -0,0 +1,11 @@ +query SkippingEmptyFragmentQuery { + computer(id: "Computer1") { + ... on Computer @defer(label: "a") { + ... on Computer @defer(label: "b") { + ... on Computer @defer(label: "c") { + id + } + } + } + } +} diff --git a/tests/defer/src/commonMain/graphql/noTypename/schema.graphqls b/tests/defer/src/commonMain/graphql/noTypename/schema.graphqls new file mode 100644 index 00000000000..0892f0f4075 --- /dev/null +++ b/tests/defer/src/commonMain/graphql/noTypename/schema.graphqls @@ -0,0 +1,31 @@ +type Query { + computers: [Computer!]! + computer(id: ID!): Computer +} + +type Mutation { + computers: [Computer!]! +} + +type Subscription { + count(to: Int!): Counter! +} + +type Counter { + value: Int! + valueTimesTwo: Int! +} + +type Computer { + id: ID! + cpu: String! + year: Int! + screen: Screen! + errorField: String + nonNullErrorField: String! +} + +type Screen { + resolution: String! + isColor: Boolean! +} diff --git a/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt index 91bea1f6f93..e6bced95233 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.ApolloRequest import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.Error.Builder import com.apollographql.apollo.api.Operation import com.apollographql.apollo.cache.normalized.ApolloStore import com.apollographql.apollo.cache.normalized.FetchPolicy @@ -72,9 +73,8 @@ class DeferNormalizedCacheTest { // Fill the cache by doing a network only request val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) apolloClient.query(WithFragmentSpreadsQuery()).fetchPolicy(FetchPolicy.NetworkOnly).toFlow().collect() @@ -86,9 +86,20 @@ class DeferNormalizedCacheTest { // We get the last/fully formed data val cacheExpected = WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ) assertEquals(cacheExpected, cacheActual) } @@ -99,9 +110,8 @@ class DeferNormalizedCacheTest { // Fill the cache by doing a first request val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) apolloClient.query(WithFragmentSpreadsQuery()).fetchPolicy(FetchPolicy.NetworkOnly).toFlow().collect() @@ -114,16 +124,26 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ), ) assertEquals(networkExpected, networkActual) @@ -134,9 +154,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheFirst).build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -148,16 +167,26 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ), ) assertEquals(networkExpected, networkActual) @@ -176,9 +205,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -188,16 +216,26 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ), ) assertEquals(networkExpected, networkActual) @@ -216,9 +254,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheAndNetwork).build() val jsonList1 = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"pending":[{"id":"0","path":["computers",0]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"isColor":false},"id":"2"}],"completed":[{"id":"0"},{"id":"2"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList1) @@ -232,10 +269,6 @@ class DeferNormalizedCacheTest { WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) - ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", @@ -245,9 +278,8 @@ class DeferNormalizedCacheTest { assertEquals(networkExpected, networkActual) val jsonList2 = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"0"},{"data":{"isColor":true},"id":"2"}],"completed":[{"id":"0"},{"id":"2"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList2) @@ -262,10 +294,6 @@ class DeferNormalizedCacheTest { WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null)) ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null)))) - ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", @@ -281,9 +309,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheFirst).build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":null,"path":["computers",0,"screen"],"label":"b","errors":[{"message":"Cannot resolve isColor","locations":[{"line":1,"column":119}],"path":["computers",0,"screen","isColor"]}]}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2","errors":[{"message":"Error field","locations":[{"line":3,"column":35}],"path":["computers",0,"screen","isColor"]}]},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -299,36 +326,40 @@ class DeferNormalizedCacheTest { query, uuid, ).data(WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - )).build(), + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ) + ).build(), - ApolloResponse.Builder( - query, - uuid, - ).data(WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) - )).build(), ApolloResponse.Builder( query, uuid, - ) - .data( - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) - ) - ) - .errors( + ).data( + WithFragmentSpreadsQuery.Data( listOf( - Error.Builder(message = "Cannot resolve isColor") - .locations(listOf(Error.Location(1, 119))) - .path(listOf("computers", 0, "screen", "isColor")) - .build() + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), ) ) - .build(), + ).errors( + listOf( + Builder("Error field") + .locations(listOf(Error.Location(3, 35))) + .path(listOf("computers", 0, "screen", "isColor")) + .build() + ) + ).build() ) assertResponseListEquals(networkExpected, networkActual) @@ -337,7 +368,7 @@ class DeferNormalizedCacheTest { val exception = apolloClient.query(WithFragmentSpreadsQuery()).execute().exception check(exception is CacheMissException) assertIs(exception.suppressedExceptions.first()) - assertEquals("Object 'computers.0.screen' has no field named 'isColor'", exception.message) + assertEquals("Object 'computers.0' has no field named 'cpu'", exception.message) mockServer.awaitRequest() } @@ -404,9 +435,8 @@ class DeferNormalizedCacheTest { @Test fun mutation() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0],"label":"c"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0],"label":"c"},{"id":"1","path":["computers",1],"label":"c"}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val networkActual = apolloClient.mutation(WithFragmentSpreadsMutation()).toFlow().toList().map { it.dataOrThrow() } @@ -414,16 +444,25 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsMutation.Data( - listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsMutation.Data( - listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsMutation.Data( listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ) + ) ), ) assertEquals(networkExpected, networkActual) @@ -433,9 +472,20 @@ class DeferNormalizedCacheTest { // We get the last/fully formed data val cacheExpected = WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ) assertEquals(cacheExpected, cacheActual) } @@ -443,9 +493,8 @@ class DeferNormalizedCacheTest { @Test fun mutationWithOptimisticDataFails() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0],"label":"c"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0],"label":"c"},{"id":"1","path":["computers",1],"label":"c"}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val responses = apolloClient.mutation(WithFragmentSpreadsMutation()).optimisticUpdates( @@ -468,8 +517,8 @@ class DeferNormalizedCacheTest { return@runTest } val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]}""", ) val multipartBody = mockServer.enqueueMultipart("application/json") multipartBody.enqueuePart(jsonList[0].encodeUtf8(), false) diff --git a/tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt deleted file mode 100644 index 2942cac0b13..00000000000 --- a/tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package test - -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.network.websocket.WebSocketNetworkTransport -import com.apollographql.apollo.testing.internal.runTest -import defer.WithFragmentSpreadsSubscription -import defer.WithInlineFragmentsSubscription -import defer.fragment.CounterFields -import kotlinx.coroutines.flow.toList -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertEquals - -/** - * This test is ignored on the CI because it requires a specific server to run. - * - * It can be manually tested by running the server from https://github.com/BoD/DeferDemo/tree/master/helix - */ -@Ignore -class DeferSubscriptionsTest { - private lateinit var apolloClient: ApolloClient - - private fun setUp() { - apolloClient = ApolloClient.Builder() - .serverUrl("http://localhost:4000/graphql") - .subscriptionNetworkTransport( - WebSocketNetworkTransport.Builder() - .serverUrl("ws://localhost:4000/graphql") - .build() - ) - .build() - } - - private fun tearDown() { - apolloClient.close() - } - - @Test - fun subscriptionWithInlineFragment() = runTest(before = { setUp() }, after = { tearDown() }) { - val expectedDataList = listOf( - // Emission 0, deferred payload 0 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 1, null)), - // Emission 0, deferred payload 1 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 1, WithInlineFragmentsSubscription.OnCounter(2))), - // Emission 1, deferred payload 0 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 2, null)), - // Emission 1, deferred payload 1 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 2, WithInlineFragmentsSubscription.OnCounter(4))), - // Emission 2, deferred payload 0 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 3, null)), - // Emission 2, deferred payload 1 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 3, WithInlineFragmentsSubscription.OnCounter(6))), - ) - - val actualDataList = apolloClient.subscription(WithInlineFragmentsSubscription()).toFlow().toList().map { it.dataOrThrow() } - assertEquals(expectedDataList, actualDataList) - } - - @Test - fun subscriptionWithFragmentSpreads() = runTest(before = { setUp() }, after = { tearDown() }) { - val expectedDataList = listOf( - // Emission 0, deferred payload 0 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 1, null)), - // Emission 0, deferred payload 1 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 1, CounterFields(2))), - // Emission 1, deferred payload 0 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 2, null)), - // Emission 1, deferred payload 1 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 2, CounterFields(4))), - // Emission 2, deferred payload 0 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 3, null)), - // Emission 2, deferred payload 1 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 3, CounterFields(6))), - ) - - val actualDataList = apolloClient.subscription(WithFragmentSpreadsSubscription()).toFlow().toList().map { it.dataOrThrow() } - assertEquals(expectedDataList, actualDataList) - } - -} diff --git a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferTest.kt index d04b2dc3b79..9d7a80a4d00 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferTest.kt @@ -3,6 +3,7 @@ package test import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.Error.Builder import com.apollographql.apollo.autoPersistedQueryInfo import com.apollographql.apollo.mpp.currentTimeMillis import com.apollographql.apollo.testing.internal.runTest @@ -43,11 +44,8 @@ class DeferTest { @Test fun deferWithFragmentSpreads() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) val expectedDataList = listOf( @@ -57,38 +55,20 @@ class DeferTest { WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) ), - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), - ) - ), - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null))), - ) - ), WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null))), - ) - ), - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ), ) @@ -101,11 +81,8 @@ class DeferTest { @Test fun deferWithInlineFragments() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"b"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"b"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"b"},{"id":"3","path":["computers",1,"screen"],"label":"b"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) val expectedDataList = listOf( @@ -115,38 +92,20 @@ class DeferTest { WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), ) ), - WithInlineFragmentsQuery.Data( - listOf( - WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", null))), - WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), - ) - ), - WithInlineFragmentsQuery.Data( - listOf( - WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", null))), - WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, - WithInlineFragmentsQuery.Screen("Screen", "800x600", null))), - ) - ), - WithInlineFragmentsQuery.Data( - listOf( - WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", - WithInlineFragmentsQuery.OnScreen(false)))), - WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, - WithInlineFragmentsQuery.Screen("Screen", "800x600", null))), - ) - ), WithInlineFragmentsQuery.Data( listOf( WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, WithInlineFragmentsQuery.Screen("Screen", "640x480", - WithInlineFragmentsQuery.OnScreen(false)))), + WithInlineFragmentsQuery.OnScreen(false) + ) + ) + ), WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, WithInlineFragmentsQuery.Screen("Screen", "800x600", - WithInlineFragmentsQuery.OnScreen(true)))), + WithInlineFragmentsQuery.OnScreen(true) + ) + ) + ), ) ), ) @@ -159,11 +118,8 @@ class DeferTest { @Test fun deferWithFragmentSpreadsAndError() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":null,"path":["computers",0,"screen"],"label":"b","errors":[{"message":"Cannot resolve isColor","locations":[{"line":1,"column":119}],"path":["computers",0,"screen","isColor"]}]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2","errors":[{"message":"Error field","locations":[{"line":3,"column":35}],"path":["computers",0,"screen","isColor"]}]},{"id":"3"}]}""", ) val query = WithFragmentSpreadsQuery() @@ -178,58 +134,10 @@ class DeferTest { WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) - )).build(), - - ApolloResponse.Builder( - query, - uuid, - ).data( - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), - ) - ) - ).build(), - - ApolloResponse.Builder( - query, - uuid, ) - .data( - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), - ) - ) - ) - .errors( - listOf( - Error.Builder(message = "Cannot resolve isColor") - .locations(listOf(Error.Location(1, 119))) - .path(listOf("computers", 0, "screen", "isColor")) - .build() - ) - ) - .build(), - - ApolloResponse.Builder( - query, - uuid, - ).data( - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null))), - ) - ) ).build(), + ApolloResponse.Builder( query, uuid, @@ -237,13 +145,25 @@ class DeferTest { WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), + ComputerFields.Screen("Screen", "640x480", null) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ) - ).build(), + ).errors( + listOf( + Builder("Error field") + .locations(listOf(Error.Location(3, 35))) + .path(listOf("computers", 0, "screen", "isColor")) + .build() + ) + ).build() ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -270,11 +190,8 @@ class DeferTest { } val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) jsonList.withIndex().forEach { (index, value) -> @@ -292,21 +209,27 @@ class DeferTest { @Test fun emptyPayloadsAreIgnored() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonWithEmptyPayload = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":true}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]}""", """{"hasNext":false}""", ) val jsonWithoutEmptyPayload = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]}""", ) val expectedDataList = listOf( SimpleDeferQuery.Data( - listOf(SimpleDeferQuery.Computer("Computer", "computer1", null)) + listOf( + SimpleDeferQuery.Computer("Computer", "Computer1", null), + SimpleDeferQuery.Computer("Computer", "Computer2", null), + ) ), SimpleDeferQuery.Data( - listOf(SimpleDeferQuery.Computer("Computer", "computer1", SimpleDeferQuery.OnComputer("386"))) + listOf( + SimpleDeferQuery.Computer("Computer", "Computer1", SimpleDeferQuery.OnComputer("386")), + SimpleDeferQuery.Computer("Computer", "Computer2", SimpleDeferQuery.OnComputer("486")), + ) ), ) @@ -327,11 +250,8 @@ class DeferTest { .build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val finalResponse = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().last() @@ -341,10 +261,16 @@ class DeferTest { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ), finalResponse.dataOrThrow() @@ -360,11 +286,8 @@ class DeferTest { mockServer.enqueueString("""{"errors":[{"message":"PersistedQueryNotFound"}]}""") val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val finalResponse = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().last() @@ -374,10 +297,16 @@ class DeferTest { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ), finalResponse.dataOrThrow() diff --git a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt new file mode 100644 index 00000000000..1746e941c95 --- /dev/null +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -0,0 +1,652 @@ +package test + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.Optional +import com.apollographql.apollo.testing.internal.runTest +import com.benasher44.uuid.uuid4 +import defer.CanDeferFragmentsOnTheTopLevelQueryFieldQuery +import defer.CanDisableDeferUsingIfArgumentQuery +import defer.DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query +import defer.DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query +import defer.DoesNotDisableDeferWithNullIfArgumentQuery +import defer.HandlesErrorsThrownInDeferredFragmentsQuery +import defer.HandlesNonNullableErrorsThrownInDeferredFragmentsQuery +import defer.HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery +import defer.NestedStreamQuery +import defer.Overlapping2Query +import defer.OverlappingQuery +import defer.SimpleStreamQuery +import defer.SubPathQuery +import defer.WithFragmentSpreadsMutation +import defer.WithFragmentSpreadsQuery +import defer.WithInlineFragmentsQuery +import defer.fragment.ComputerErrorField +import defer.fragment.ComputerFields +import defer.fragment.FragmentOnQuery +import defer.fragment.ScreenFields +import defer.notypename.SkippingEmptyFragmentQuery +import kotlinx.coroutines.flow.toList +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * End-to-end tests for `@defer`. + * + * These tests are not run by default (they are excluded in the gradle conf) because they expect an instance of + * [Apollo Server](https://www.apollographql.com/docs/apollo-server) running locally. + * + * They are enabled only when running from the specific `defer-with-apollo-server-tests` CI workflow. + */ +class DeferWithApolloServerTest { + private lateinit var apolloClient: ApolloClient + + private fun setUp() { + apolloClient = ApolloClient.Builder() + .serverUrl("http://127.0.0.1:4000/") + .build() + } + + private fun tearDown() { + apolloClient.close() + } + + @Test + fun deferWithFragmentSpreads() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]} + val expectedDataList = listOf( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ), + ) + + val actualDataList = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferWithInlineFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"b"},{"id":"3","path":["computers",1,"screen"],"label":"b"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]} + val expectedDataList = listOf( + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", null), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, + WithInlineFragmentsQuery.Screen("Screen", "640x480", + WithInlineFragmentsQuery.OnScreen(false) + ) + ) + ), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, + WithInlineFragmentsQuery.Screen("Screen", "800x600", + WithInlineFragmentsQuery.OnScreen(true) + ) + ) + ), + ) + ), + ) + val actualDataList = apolloClient.query(WithInlineFragmentsQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferWithFragmentSpreadsMutation() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0],"label":"c"},{"id":"1","path":["computers",1],"label":"c"}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]} + val expectedDataList = listOf( + WithFragmentSpreadsMutation.Data( + listOf( + WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", null), + ) + ), + WithFragmentSpreadsMutation.Data( + listOf( + WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ), + ) + + val actualDataList = apolloClient.mutation(WithFragmentSpreadsMutation()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun canDisableDeferUsingIfArgument() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1","cpu":"386"},{"__typename":"Computer","id":"Computer2","cpu":"486"}]} + val expectedDataList = listOf( + CanDisableDeferUsingIfArgumentQuery.Data( + listOf( + CanDisableDeferUsingIfArgumentQuery.Computer("Computer", "Computer1", CanDisableDeferUsingIfArgumentQuery.OnComputer("386")), + CanDisableDeferUsingIfArgumentQuery.Computer("Computer", "Computer2", CanDisableDeferUsingIfArgumentQuery.OnComputer("486")), + ) + ), + ) + val actualDataList = apolloClient.query(CanDisableDeferUsingIfArgumentQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun doesNotDisableDeferWithNullIfArgument() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]} + val expectedDataList = listOf( + DoesNotDisableDeferWithNullIfArgumentQuery.Data( + listOf( + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer1", null), + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer2", null), + ) + ), + DoesNotDisableDeferWithNullIfArgumentQuery.Data( + listOf( + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer1", DoesNotDisableDeferWithNullIfArgumentQuery.OnComputer("386")), + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer2", DoesNotDisableDeferWithNullIfArgumentQuery.OnComputer("486")), + ) + ) + ) + val actualDataList = + apolloClient.query(DoesNotDisableDeferWithNullIfArgumentQuery(Optional.Absent)).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun canDeferFragmentsOnTheTopLevelQueryField() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"__typename":"Query"},"pending":[{"id":"0","path":[]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"computers":[{"id":"Computer1"},{"id":"Computer2"}]},"id":"0"}],"completed":[{"id":"0"}]} + val expectedDataList = listOf( + CanDeferFragmentsOnTheTopLevelQueryFieldQuery.Data( + "Query", + null + ), + CanDeferFragmentsOnTheTopLevelQueryFieldQuery.Data( + "Query", + FragmentOnQuery( + listOf( + FragmentOnQuery.Computer("Computer1"), + FragmentOnQuery.Computer("Computer2"), + ) + ) + ), + ) + val actualDataList = apolloClient.query(CanDeferFragmentsOnTheTopLevelQueryFieldQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferFragmentThatIsAlsoNotDeferredIsSkipped1() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"screen":{"__typename":"Screen","isColor":false}}}} + val expectedDataList = listOf( + DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query.Data( + DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query.Computer( + DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query.Screen("Screen", ScreenFields(false)) + ) + ), + ) + val actualDataList = apolloClient.query(DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferFragmentThatIsAlsoNotDeferredIsSkipped2() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"screen":{"__typename":"Screen","isColor":false}}}} + val expectedDataList = listOf( + DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query.Data( + DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query.Computer( + DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query.Screen("Screen", ScreenFields(false)) + ) + ), + ) + val actualDataList = apolloClient.query(DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun handlesErrorsThrownInDeferredFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"errorField":null},"errors":[{"message":"Error field","locations":[{"line":3,"column":43}],"path":["computer","errorField"],"extensions":{"code":"INTERNAL_SERVER_ERROR","stacktrace":["Error: Error field"," at Object.errorField (file:///Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/computers.js:29:19)"," at field.resolve (file:///Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/@apollo/server/dist/esm/utils/schemaInstrumentation.js:36:28)"," at executeField (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:567:20)"," at executeFields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:476:22)"," at executeExecutionGroup (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1855:14)"," at executor (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1803:7)"," at pendingExecutionGroup.result (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1825:58)"," at IncrementalGraph._onExecutionGroup (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/IncrementalGraph.js:192:33)"," at IncrementalGraph._promoteNonEmptyToRoot (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/IncrementalGraph.js:146:20)"," at IncrementalGraph.getNewRootNodes (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/IncrementalGraph.js:25:17)"]}}],"id":"0"}],"completed":[{"id":"0"}]} + val query = HandlesErrorsThrownInDeferredFragmentsQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ) + .data( + HandlesErrorsThrownInDeferredFragmentsQuery.Data( + HandlesErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", null + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ) + .data( + HandlesErrorsThrownInDeferredFragmentsQuery.Data( + HandlesErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", ComputerErrorField(null) + ) + ) + ) + .errors( + listOf( + Error.Builder(message = "Error field") + .path(listOf("computer", "errorField")) + .build() + ) + ) + .build(), + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun handlesNonNullableErrorsThrownInDeferredFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"]}],"hasNext":true} + // {"hasNext":false,"completed":[{"id":"0","errors":[{"message":"Cannot return null for non-nullable field Computer.nonNullErrorField.","locations":[{"line":3,"column":54}],"path":["computer","nonNullErrorField"]}]}]} + val query = HandlesNonNullableErrorsThrownInDeferredFragmentsQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", null + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ) + .data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", null + ) + ) + ) + .errors(listOf(Error.Builder(message = "Cannot return null for non-nullable field Computer.nonNullErrorField.") + .path(listOf("computer", "nonNullErrorField")).build() + ) + ) + .build(), + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun handlesNonNullableErrorsThrownOutsideDeferredFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"errors":[{"message":"Cannot return null for non-nullable field Computer.nonNullErrorField.","locations":[{"line":1,"column":108}],"path":["computer","nonNullErrorField"],"extensions":{"code":"INTERNAL_SERVER_ERROR","stacktrace":["Error: Cannot return null for non-nullable field Computer.nonNullErrorField."," at completeValue (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:716:13)"," at executeField (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:580:23)"," at executeFields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:476:22)"," at collectAndExecuteSubfields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1491:21)"," at completeObjectValue (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1395:10)"," at completeValue (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:760:12)"," at executeField (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:580:23)"," at executeFields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:476:22)"," at executeRootGroupedFieldSet (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:373:14)"," at executeOperation (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:159:30)"]}}],"data":{"computer":null}} + val query = HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery.Data( + null + ) + ) + .errors( + listOf( + Error.Builder(message = "Cannot return null for non-nullable field Computer.nonNullErrorField.") + .path(listOf("computer", "nonNullErrorField")) + .build() + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun overlapping() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"],"label":"b"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"cpu":"386","year":1993},"id":"0"}],"completed":[{"id":"0"}]} + val query = OverlappingQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + OverlappingQuery.Data( + OverlappingQuery.Computer( + "Computer", "Computer1", OverlappingQuery.OnComputer( + "Computer", "Computer1", null, + ) + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + OverlappingQuery.Data( + OverlappingQuery.Computer( + "Computer", "Computer1", OverlappingQuery.OnComputer( + "Computer", "Computer1", OverlappingQuery.OnComputer1("Computer1", "386", 1993) + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun overlapping2() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"],"label":"b"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"cpu":"386","year":1993},"id":"0"}],"completed":[{"id":"0"}]} + val query = Overlapping2Query() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + Overlapping2Query.Data( + Overlapping2Query.Computer( + "Computer", "Computer1", Overlapping2Query.OnComputerDeferA("Computer1" + ), null + ) + ) + ) + .build(), + ApolloResponse.Builder( + query, + uuid, + ).data( + Overlapping2Query.Data( + Overlapping2Query.Computer( + "Computer", "Computer1", Overlapping2Query.OnComputerDeferA("Computer1" + ), Overlapping2Query.OnComputerDeferB( + "Computer1", "386", 1993 + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun subPath() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"__typename":"Query","computer":{"id":"Computer1"}},"pending":[{"id":"0","path":[],"label":"a"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"screen":{"isColor":false}},"id":"0","subPath":["computer"]},{"data":{"MyFragment":"Query"},"id":"0"}],"completed":[{"id":"0"}]} + val query = SubPathQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SubPathQuery.Data( + "Query", SubPathQuery.Computer( + "Computer1" + ), null + ) + ) + .build(), + ApolloResponse.Builder( + query, + uuid, + ).data( + SubPathQuery.Data( + "Query", SubPathQuery.Computer( + "Computer1" + ), SubPathQuery.OnQuery( + "Query", SubPathQuery.Computer1( + "Computer1", + SubPathQuery.Screen(false + ) + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun skippingEmptyFragment() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{}},"pending":[{"id":"0","path":["computer"],"label":"c"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"id":"Computer1"},"id":"0"}],"completed":[{"id":"0"}]} + val query = SkippingEmptyFragmentQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SkippingEmptyFragmentQuery.Data( + SkippingEmptyFragmentQuery.Computer( + SkippingEmptyFragmentQuery.OnComputer( + SkippingEmptyFragmentQuery.OnComputer1( + null + ) + ) + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + SkippingEmptyFragmentQuery.Data( + SkippingEmptyFragmentQuery.Computer( + SkippingEmptyFragmentQuery.OnComputer( + SkippingEmptyFragmentQuery.OnComputer1( + SkippingEmptyFragmentQuery.OnComputer2( + "Computer1" + ) + ) + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun simpleStream0Initial() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[]},"pending":[{"id":"0","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"id":"0","items":[{"id":"Computer1"},{"id":"Computer2"}]}],"completed":[{"id":"0"}]} + val query = SimpleStreamQuery(0) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf() + ) + ) + .build(), + + + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + SimpleStreamQuery.Computer("Computer2"), + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun simpleStream1Initial() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"id":"Computer1"}]},"pending":[{"id":"0","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"id":"0","items":[{"id":"Computer2"}]}],"completed":[{"id":"0"}]} + val query = SimpleStreamQuery(1) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + SimpleStreamQuery.Computer("Computer2"), + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun nestedStream() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"id":"Computer1","peripherals":["Keyboard"]}]},"pending":[{"id":"0","path":["computers",0,"peripherals"]},{"id":"1","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",1,"peripherals"]}],"incremental":[{"id":"0","items":["Mouse","Printer"]},{"id":"1","items":[{"id":"Computer2","peripherals":["Keyboard"]}]},{"id":"2","items":["Mouse","Printer","Scanner"]}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"}]} + val query = NestedStreamQuery(1) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + NestedStreamQuery.Data( + listOf( + NestedStreamQuery.Computer("Computer1", listOf("Keyboard")) + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + NestedStreamQuery.Data( + listOf( + NestedStreamQuery.Computer("Computer1", listOf("Keyboard", "Mouse", "Printer")), + NestedStreamQuery.Computer("Computer2", listOf("Keyboard", "Mouse", "Printer", "Scanner")) + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } +} diff --git a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt index 40d36185026..03a39f6e738 100644 --- a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt +++ b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt @@ -4,11 +4,11 @@ import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.cache.http.HttpFetchPolicy import com.apollographql.apollo.cache.http.httpCache import com.apollographql.apollo.cache.http.httpFetchPolicy -import com.apollographql.mockserver.MockServer -import com.apollographql.mockserver.enqueueMultipart import com.apollographql.apollo.mpp.currentTimeMillis import com.apollographql.apollo.testing.awaitElement import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.mockserver.MockServer +import com.apollographql.mockserver.enqueueMultipart import defer.WithFragmentSpreadsQuery import defer.fragment.ComputerFields import defer.fragment.ScreenFields @@ -60,11 +60,8 @@ class DeferJvmTest { } val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental":[{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental":[{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental":[{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) for ((index, json) in jsonList.withIndex()) { @@ -83,10 +80,14 @@ class DeferJvmTest { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ComputerFields.Screen("Screen", "800x600", ScreenFields(true)) + ) + ), ) )