From 516071185c3e2bc7482366b23e35e726303526a9 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 13 Dec 2024 18:30:09 +0100 Subject: [PATCH 1/6] Update `DeferredJsonMerger` to take `pending` and `completed` into account. --- .../apollo/internal/DeferredJsonMerger.kt | 101 +- .../test/defer/DeferredJsonMergerTest.kt | 1375 ++++++++++++----- 2 files changed, 1093 insertions(+), 383 deletions(-) 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 index b034caecc28..f2eb734ad82 100644 --- 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 @@ -15,19 +15,25 @@ private typealias MutableJsonMap = MutableMap * 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). + * 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 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 + * `errors` in incremental and completed 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` field of the [merged] + * Map, at each call to [merge]. */ @ApolloInternal +@Suppress("UNCHECKED_CAST") class DeferredJsonMerger { private val _merged: MutableJsonMap = mutableMapOf() val merged: JsonMap = _merged + /** + * Map of identifiers to their corresponding DeferredFragmentIdentifier, found in `pending`. + */ + private val idsToDeferredFragmentIdentifiers = mutableMapOf() + private val _mergedFragmentIds = mutableSetOf() val mergedFragmentIds: Set = _mergedFragmentIds @@ -47,11 +53,12 @@ class DeferredJsonMerger { return merge(payloadMap) } - @Suppress("UNCHECKED_CAST") fun merge(payload: JsonMap): JsonMap { if (merged.isEmpty()) { - // Initial payload, no merging needed - _merged += payload + // Initial payload, no merging needed (strip some fields that should not appear in the final result) + _merged += payload - "hasNext" - "pending" + handlePending(payload) + handleCompleted(payload) return merged } @@ -60,48 +67,68 @@ class DeferredJsonMerger { 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") + mergeIncrementalData(incrementalItem) + // Merge errors (if any) of the incremental item + (incrementalItem["errors"] as? List)?.let { getOrPutMergedErrors() += it } } } hasNext = payload["hasNext"] as Boolean? ?: false + handlePending(payload) + handleCompleted(payload) + + (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } + 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 + private fun getOrPutMergedErrors() = _merged.getOrPut("errors") { mutableListOf() } as MutableList - // payloadData can be null if there are errors - if (data != null) { - val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap - deepMerge(nodeToMergeInto, data) + private fun getOrPutExtensions() = _merged.getOrPut("extensions") { mutableMapOf() } as MutableJsonMap - _mergedFragmentIds += DeferredFragmentIdentifier(path = path, label = incrementalItem["label"] as String?) + private fun handlePending(payload: JsonMap) { + val pending = payload["pending"] as? List + if (pending != null) { + for (pendingItem in pending) { + val id = pendingItem["id"] as String + val path = pendingItem["path"] as List + val label = pendingItem["label"] as String? + idsToDeferredFragmentIdentifiers[id] = DeferredFragmentIdentifier(path = path, label = label) + } } } - @Suppress("UNCHECKED_CAST") + private fun handleCompleted(payload: JsonMap) { + val completed = payload["completed"] as? List + if (completed != null) { + for (completedItem in completed) { + val errors = completedItem["errors"] as? List + if (errors != null) { + // Merge errors (if any) of the completed item + getOrPutMergedErrors() += errors + } else { + // No errors: we have merged all the fields of the fragment so it can be parsed + val id = completedItem["id"] as String + val deferredFragmentIdentifier = idsToDeferredFragmentIdentifiers.remove(id) + ?: error("Id '$id' not found in pending results") + _mergedFragmentIds += deferredFragmentIdentifier + } + } + } + } + + private fun mergeIncrementalData(incrementalItem: JsonMap) { + val id = incrementalItem["id"] as String? ?: error("No id found in incremental item") + val data = incrementalItem["data"] as JsonMap? ?: error("No data found in incremental item") + val subPath = incrementalItem["subPath"] as List? ?: emptyList() + val path = (idsToDeferredFragmentIdentifiers[id]?.path ?: error("Id '$id' not found in pending results")) + subPath + val mergedData = merged["data"] as JsonMap + val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap + deepMerge(nodeToMergeInto, data) + } + private fun deepMerge(destination: MutableJsonMap, map: JsonMap) { for ((key, value) in map) { if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { @@ -116,7 +143,6 @@ class DeferredJsonMerger { } } - @Suppress("UNCHECKED_CAST") private fun jsonToMap(json: BufferedSource): JsonMap = BufferedSourceJsonReader(json).readAny() as JsonMap @@ -130,7 +156,6 @@ class DeferredJsonMerger { node = if (node is List<*>) { node[key as Int] } else { - @Suppress("UNCHECKED_CAST") node as JsonMap node[key] } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt index 40918461ae3..ee2a1ff3f89 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt @@ -16,11 +16,12 @@ private fun String.buffer() = Buffer().writeUtf8(this) private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map class DeferredJsonMergerTest { - @Test - fun mergeJsonSingleIncrementalItem() { - val deferredJsonMerger = DeferredJsonMerger() + @Test + fun mergeJsonSingleIncrementalItem() { + val deferredJsonMerger = DeferredJsonMerger() - val payload1 = """ + //language=JSON + val payload1 = """ { "data": { "computers": [ @@ -38,14 +39,45 @@ class DeferredJsonMergerTest { } ] }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + } + ], "hasNext": true } """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + } + } + """ deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(payload1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) - + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + //language=JSON val payload2 = """ { "incremental": [ @@ -57,22 +89,34 @@ class DeferredJsonMergerTest { "resolution": "640x480" } }, + "id": "0" + } + ], + "completed": [ + { + "id": "0" + } + ], + "pending": [ + { + "id": "1", "path": [ "computers", - 0 + 1 ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - } + "label": "query:Query1:0" } ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, "hasNext": true } """ + //language=JSON val mergedPayloads_1_2 = """ { "data": { @@ -94,54 +138,64 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, "extensions": { - "incremental": [ - { - "duration": { - "amount": 100, - "unit": "ms" - } - } - ] + "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 - ) - + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + ), + deferredJsonMerger.mergedFragmentIds + ) + //language=JSON val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } + { + "incremental": [ + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" } - } - ], - "hasNext": true - } + }, + "id": "1" + } + ], + "completed": [ + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + }, + "hasNext": true + } """ + //language=JSON val mergedPayloads_1_2_3 = """ { "data": { @@ -166,61 +220,64 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, "extensions": { - "incremental": [ - { - "duration": { - "amount": 25, - "unit": "ms" - } - } - ] + "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 - ) - + 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 + ) + //language=JSON 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 - } + { + "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 + } """ + //language=JSON val mergedPayloads_1_2_3_4 = """ { "data": { @@ -245,7 +302,6 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, "errors": [ { "message": "Cannot resolve isColor", @@ -262,54 +318,63 @@ class DeferredJsonMergerTest { "isColor" ] } - ] + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + } } """ 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 - ) - + 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 + ) + //language=JSON 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" - } + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] } - } - ], - "hasNext": false - } + ] + } + ], + "completed": [ + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } """ + //language=JSON val mergedPayloads_1_2_3_4_5 = """ { "data": { @@ -335,19 +400,22 @@ class DeferredJsonMergerTest { } ] }, - "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": [ @@ -357,24 +425,34 @@ class DeferredJsonMergerTest { } ] } - ] + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } } """ - 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 - ) - } + 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() + @Test + fun mergeJsonMultipleIncrementalItems() { + val deferredJsonMerger = DeferredJsonMerger() - val payload1 = """ + //language=JSON + val payload1 = """ { "data": { "computers": [ @@ -392,151 +470,166 @@ class DeferredJsonMergerTest { } ] }, - "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" - } + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" }, - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" } - } - ], - "hasNext": true - } + ], + "hasNext": true + } """ - val mergedPayloads_1_2_3 = """ + //language=JSON + val mergedPayloads_1 = """ { "data": { "computers": [ { "id": "Computer1", - "cpu": "386", - "year": 1993, "screen": { - "isTouch": true, - "resolution": "640x480" + "isTouch": true } }, { "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" + "isTouch": false } } ] } } """ - 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 - ) - + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) - val payload4_5 = """ + //language=JSON + val payload2_3 = """ { "incremental": [ { - "data": null, + "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" ], - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], "label": "fragment:ComputerFields:0" }, { - "data": { - "isColor": false - }, + "id": "3", "path": [ "computers", 1, "screen" ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """ + //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" + } + } + } + """ + 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 + ) + + //language=JSON + val payload4_5 = """ + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", "errors": [ { "message": "Another error", @@ -547,21 +640,46 @@ class DeferredJsonMergerTest { } ] } - ], - "label": "fragment:ComputerFields:0", - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" + ] + } + ], + "completed": [ + { + "id": "2", + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] } - } + ] + }, + { + "id": "3" } ], - "hasNext": true + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false } """ - val mergedPayloads_1_2_3_4_5 = """ + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ { "data": { "computers": [ @@ -586,19 +704,16 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - } - ] - }, "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + }, { "message": "Cannot resolve isColor", "locations": [ @@ -613,34 +728,35 @@ class DeferredJsonMergerTest { "screen", "isColor" ] - }, - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] } - ] + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } } """ - 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 - ) - } + 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() + @Test + fun emptyPayloads() { + val deferredJsonMerger = DeferredJsonMerger() - val payload1 = """ + //language=JSON + val payload1 = """ { "data": { "computers": [ @@ -658,21 +774,40 @@ class DeferredJsonMergerTest { } ] }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], "hasNext": true } """ - deferredJsonMerger.merge(payload1.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + deferredJsonMerger.merge(payload1.buffer()) + assertFalse(deferredJsonMerger.isEmptyPayload) - val payload2 = """ + //language=JSON + val payload2 = """ { "hasNext": true } """ - deferredJsonMerger.merge(payload2.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) - - val payload3 = """ + deferredJsonMerger.merge(payload2.buffer()) + assertTrue(deferredJsonMerger.isEmptyPayload) + //language=JSON + val payload3 = """ { "incremental": [ { @@ -683,31 +818,581 @@ class DeferredJsonMergerTest { "resolution": "640x480" } }, - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" + "id": "0" + } + ], + "hasNext": true + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertFalse(deferredJsonMerger.isEmptyPayload) + + //language=JSON + val payload4 = """ + { + "hasNext": false + } + """ + deferredJsonMerger.merge(payload4.buffer()) + assertTrue(deferredJsonMerger.isEmptyPayload) + } + + /** + * Example A from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleA() { + val deferredJsonMerger = DeferredJsonMerger() + //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 + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { "h": "h", "i": "i" } + } + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { "id": "0", "data": { "MyFragment": "Query" } }, + { "id": "0", "subPath": ["f2", "c", "f"], "data": { "j": "j" } } + ], + "completed": [{ "id": "0" }], + "hasNext": false + } + """ + //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" + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = null), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example A2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleA2() { + val deferredJsonMerger = DeferredJsonMerger() + //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 + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" } } } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //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 } """ - deferredJsonMerger.merge(payload3.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + //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" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "D1"), + ), + deferredJsonMerger.mergedFragmentIds + ) - val payload4 = """ + //language=JSON + val payload3 = """ { + "incremental": [ + {"id": "1", "data": {"l": "L", "m": "M"}} + ], + "completed": [ + {"id": "1"} + ], "hasNext": false } """ - deferredJsonMerger.merge(payload4.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) - } -} \ No newline at end of file + + //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" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "D1"), + DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example B1 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleB1() { + val deferredJsonMerger = DeferredJsonMerger() + //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 + } + """ + + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } }, + { "id": "1", "data": { "e": { "f": "f" } } } + ], + "completed": [{ "id": "1" }], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.mergedFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } } + ], + "completed": [{ "id": "0" }], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example B2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleB2() { + val deferredJsonMerger = DeferredJsonMerger() + //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 + } + """ + + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } }, + { "id": "1", "data": { "e": { "f": "f" } } } + ], + "completed": [{ "id": "0" }], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + } + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + ), + deferredJsonMerger.mergedFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } } + ], + "completed": [{ "id": "1" }], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example D from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleD() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { "me": {} }, + "pending": [ + { "path": [], "id": "0" }, + { "path": ["me"], "id": "1" } + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //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 + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "list": [ + { "item": { "id": "1" } }, + { "item": { "id": "2" } }, + { "item": { "id": "3" } } + ] + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = null), + ), + deferredJsonMerger.mergedFragmentIds + ) + + //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 + } + """ + //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" } } + ] + } + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = null), + DeferredFragmentIdentifier(path = listOf(), label = null), + ), + deferredJsonMerger.mergedFragmentIds + ) + } +} From 8de3adc40fdb9cf9fa48de9c2f27489a1c4b4f4f Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 14:44:46 +0100 Subject: [PATCH 2/6] Track pending fragment ids rather than completed ones. --- .../apollo/api/BooleanExpression.kt | 10 +- .../apollo/internal/DeferredJsonMerger.kt | 22 +- .../network/http/HttpNetworkTransport.kt | 2 +- .../websocket/WebSocketNetworkTransport.kt | 2 +- .../network/ws/WebSocketNetworkTransport.kt | 3 +- .../test/defer/DeferredJsonMergerTest.kt | 473 +++++++++++++++--- 6 files changed, 431 insertions(+), 81 deletions(-) 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 96d5607ea03..e56d8590155 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 @@ -147,16 +147,20 @@ fun BooleanExpression.evaluate( return evaluate { when (it) { is BVariable -> !(variables?.contains(it.name) ?: false) - is BLabel -> hasDeferredFragment(deferredFragmentIdentifiers, croppedPath!!, it.label) + is BLabel -> !isDeferredFragmentPending(deferredFragmentIdentifiers, croppedPath!!, it.label) is BPossibleTypes -> it.possibleTypes.contains(typename) } } } -private fun hasDeferredFragment(deferredFragmentIdentifiers: Set?, path: List, label: String?): Boolean { +private fun isDeferredFragmentPending( + deferredFragmentIdentifiers: Set?, + path: List, + label: String?, +): Boolean { if (deferredFragmentIdentifiers == 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)) } 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 index f2eb734ad82..77595a2b40d 100644 --- 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 @@ -12,7 +12,7 @@ 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 + * Each call to [merge] will merge the given chunk into the [merged] Map, and will also update the [pendingFragmentIds] Set with the * value of its `path` and `label` field. * * 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 @@ -32,10 +32,8 @@ class DeferredJsonMerger { /** * Map of identifiers to their corresponding DeferredFragmentIdentifier, found in `pending`. */ - private val idsToDeferredFragmentIdentifiers = mutableMapOf() - - private val _mergedFragmentIds = mutableSetOf() - val mergedFragmentIds: Set = _mergedFragmentIds + private val _pendingFragmentIds = mutableMapOf() + val pendingFragmentIds: Set get() = _pendingFragmentIds.values.toSet() var hasNext: Boolean = true private set @@ -95,7 +93,7 @@ class DeferredJsonMerger { val id = pendingItem["id"] as String val path = pendingItem["path"] as List val label = pendingItem["label"] as String? - idsToDeferredFragmentIdentifiers[id] = DeferredFragmentIdentifier(path = path, label = label) + _pendingFragmentIds[id] = DeferredFragmentIdentifier(path = path, label = label) } } } @@ -104,16 +102,14 @@ class DeferredJsonMerger { val completed = payload["completed"] as? List if (completed != null) { for (completedItem in completed) { + // Merge errors (if any) of the completed item val errors = completedItem["errors"] as? List if (errors != null) { - // Merge errors (if any) of the completed item getOrPutMergedErrors() += errors } else { - // No errors: we have merged all the fields of the fragment so it can be parsed + // Fragment is no longer pending - only if there were no errors val id = completedItem["id"] as String - val deferredFragmentIdentifier = idsToDeferredFragmentIdentifiers.remove(id) - ?: error("Id '$id' not found in pending results") - _mergedFragmentIds += deferredFragmentIdentifier + _pendingFragmentIds.remove(id) ?: error("Id '$id' not found in pending results") } } } @@ -123,7 +119,7 @@ class DeferredJsonMerger { val id = incrementalItem["id"] as String? ?: error("No id found in incremental item") val data = incrementalItem["data"] as JsonMap? ?: error("No data found in incremental item") val subPath = incrementalItem["subPath"] as List? ?: emptyList() - val path = (idsToDeferredFragmentIdentifiers[id]?.path ?: error("Id '$id' not found in pending results")) + subPath + val path = (_pendingFragmentIds[id]?.path ?: error("Id '$id' not found in pending results")) + subPath val mergedData = merged["data"] as JsonMap val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap deepMerge(nodeToMergeInto, data) @@ -165,7 +161,7 @@ class DeferredJsonMerger { fun reset() { _merged.clear() - _mergedFragmentIds.clear() + _pendingFragmentIds.clear() hasNext = true isEmptyPayload = false } 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 159d349c463..ce202061145 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 @@ -213,7 +213,7 @@ private constructor( jsonMerger = DeferredJsonMerger() } val merged = jsonMerger!!.merge(part) - val deferredFragmentIds = jsonMerger!!.mergedFragmentIds + val deferredFragmentIds = jsonMerger!!.pendingFragmentIds val isLast = !jsonMerger!!.hasNext if (jsonMerger!!.isEmptyPayload) { 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..1bfe1f0b98a 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 @@ -215,7 +215,7 @@ private class DefaultSubscriptionParser(private val request: } val (payload, mergedFragmentIds) = if (responseMap.isDeferred()) { - deferredJsonMerger.merge(responseMap) to deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.merge(responseMap) to deferredJsonMerger.pendingFragmentIds } else { responseMap to null } 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..8ffda6b9285 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 @@ -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]. @@ -304,7 +303,7 @@ private constructor( val responsePayload = response.payload val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters]!! val (payload, mergedFragmentIds) = if (responsePayload.isDeferred()) { - deferredJsonMerger.merge(responsePayload) to deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.merge(responsePayload) to deferredJsonMerger.pendingFragmentIds } else { responsePayload to null } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt index ee2a1ff3f89..c0a9c4cf480 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt @@ -75,7 +75,12 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -150,9 +155,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0") ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -232,10 +237,9 @@ class DeferredJsonMergerTest { 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"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -331,10 +335,10 @@ class DeferredJsonMergerTest { 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"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -439,11 +443,9 @@ class DeferredJsonMergerTest { 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"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) } @@ -514,7 +516,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2_3 = """ @@ -615,10 +623,10 @@ class DeferredJsonMergerTest { 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"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -743,11 +751,9 @@ class DeferredJsonMergerTest { 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"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) } @@ -838,7 +844,7 @@ class DeferredJsonMergerTest { } /** - * Example A from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example A from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleA() { @@ -879,7 +885,12 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = null), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -912,15 +923,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example A2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example A2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleA2() { @@ -958,7 +967,12 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "D1"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -998,9 +1012,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "D1"), + DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1042,16 +1056,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = "D1"), - DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example B1 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example B1 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleB1() { @@ -1086,7 +1097,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -1121,9 +1138,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1161,16 +1178,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example B2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example B2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleB2() { @@ -1205,7 +1219,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -1243,9 +1263,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1283,16 +1303,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example D from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example D from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleD() { @@ -1318,7 +1335,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = null), + DeferredFragmentIdentifier(path = listOf("me"), label = null), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -1354,9 +1377,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = null), + DeferredFragmentIdentifier(path = listOf(), label = null), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1388,11 +1411,339 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example F from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleF() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": {} + }, + "pending": [ + {"id": "0", "path": ["me"], "label": "B"} + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = null), - DeferredFragmentIdentifier(path = listOf(), label = null), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + {"id":"0" , "data": {"a": "A", "b": "B"}} + ], + "completed": [ + {"id": "0"} + ], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "a": "A", + "b": "B" + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds ) } + + /** + * Example G from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleG() { + val deferredJsonMerger = DeferredJsonMerger() + //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 + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [{ "name": "My Project" }] + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "Billing"), + DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34" + } + } + ], + "completed": [{ "id": "0" }], + "hasNext": true + } + """ + //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" + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "id": "1", + "data": { "previousInvoices": [{ "name": "My Invoice" }] } + } + ], + "completed": [{ "id": "1" }], + "hasNext": false + } + """ + //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" }] + } + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example H from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleH() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": {} + }, + "pending": [ + {"id": "0", "path": [], "label": "A"}, + {"id": "1", "path": ["me"], "label": "B"} + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "A"), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //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 + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //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 + } + """ + //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"] + } + ] + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + } } From c0e8caa9f55664e3bf35fb7da461ada1d4877387 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 17:46:57 +0100 Subject: [PATCH 3/6] Update more tests --- .../apollo/internal/DeferredJsonMerger.kt | 2 +- .../kotlin/test/DeferNormalizedCacheTest.kt | 239 +++++++++++------- .../kotlin/test/DeferSubscriptionsTest.kt | 80 ------ .../src/commonTest/kotlin/test/DeferTest.kt | 217 ++++++---------- 4 files changed, 218 insertions(+), 320 deletions(-) delete mode 100644 tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt 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 index 77595a2b40d..747ac84990b 100644 --- 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 @@ -59,6 +59,7 @@ class DeferredJsonMerger { handleCompleted(payload) return merged } + handlePending(payload) val incrementalList = payload["incremental"] as? List if (incrementalList == null) { @@ -74,7 +75,6 @@ class DeferredJsonMerger { hasNext = payload["hasNext"] as Boolean? ?: false - handlePending(payload) handleCompleted(payload) (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } 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() From 62bfd6ae808e625b1e2f8c5589d48d513379cce0 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 19:41:00 +0100 Subject: [PATCH 4/6] Add Apollo Server end-to-end tests --- .../defer-with-apollo-server-tests.yml | 37 ++ .../apollo/internal/DeferredJsonMerger.kt | 14 +- tests/defer/README.md | 13 + tests/defer/apollo-server/README.md | 4 + tests/defer/apollo-server/computers.graphqls | 27 ++ tests/defer/apollo-server/computers.js | 40 ++ tests/defer/apollo-server/package.json | 18 + .../patches/@apollo+server+4.11.2.patch | 28 ++ tests/defer/build.gradle.kts | 9 +- .../commonMain/graphql/base/operation.graphql | 18 + .../kotlin/test/DeferWithApolloServerTest.kt | 360 ++++++++++++++++++ 11 files changed, 559 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/defer-with-apollo-server-tests.yml create mode 100644 tests/defer/apollo-server/README.md create mode 100644 tests/defer/apollo-server/computers.graphqls create mode 100644 tests/defer/apollo-server/computers.js create mode 100644 tests/defer/apollo-server/package.json create mode 100644 tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch create mode 100644 tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt diff --git a/.github/workflows/defer-with-apollo-server-tests.yml b/.github/workflows/defer-with-apollo-server-tests.yml new file mode 100644 index 00000000000..8397a845919 --- /dev/null +++ b/.github/workflows/defer-with-apollo-server-tests.yml @@ -0,0 +1,37 @@ +name: defer-router-tests + +on: + schedule: + - cron: '0 3 * * *' +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + +jobs: + defer-with-router-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-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt index 747ac84990b..c0d0e0d1d09 100644 --- 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 @@ -52,30 +52,29 @@ class DeferredJsonMerger { } fun merge(payload: JsonMap): JsonMap { + val completed = payload["completed"] as? List if (merged.isEmpty()) { // Initial payload, no merging needed (strip some fields that should not appear in the final result) _merged += payload - "hasNext" - "pending" handlePending(payload) - handleCompleted(payload) + handleCompleted(completed) return merged } handlePending(payload) val incrementalList = payload["incremental"] as? List - if (incrementalList == null) { - isEmptyPayload = true - } else { - isEmptyPayload = false + if (incrementalList != null) { for (incrementalItem in incrementalList) { mergeIncrementalData(incrementalItem) // Merge errors (if any) of the incremental item (incrementalItem["errors"] as? List)?.let { getOrPutMergedErrors() += it } } } + isEmptyPayload = completed == null && incrementalList == null hasNext = payload["hasNext"] as Boolean? ?: false - handleCompleted(payload) + handleCompleted(completed) (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } @@ -98,8 +97,7 @@ class DeferredJsonMerger { } } - private fun handleCompleted(payload: JsonMap) { - val completed = payload["completed"] as? List + private fun handleCompleted(completed: List?) { if (completed != null) { for (completedItem in completed) { // Merge errors (if any) of the completed item 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..4c410b7fe7e --- /dev/null +++ b/tests/defer/apollo-server/computers.graphqls @@ -0,0 +1,27 @@ +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! +} + +type Screen { + resolution: String! + isColor: Boolean! +} + +directive @defer( + if: Boolean! = true + label: String +) on FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/tests/defer/apollo-server/computers.js b/tests/defer/apollo-server/computers.js new file mode 100644 index 00000000000..d5cedcd16b0 --- /dev/null +++ b/tests/defer/apollo-server/computers.js @@ -0,0 +1,40 @@ +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}}, + {id: 'Computer2', cpu: "486", year: 1996, screen: {resolution: "800x600", isColor: true}}, +] + +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 0634d784f71..40feb9e2bfb 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -86,5 +86,12 @@ tasks.withType(AbstractTestTask::class.java) { } else { filter.setExcludePatterns("test.DeferWithRouterTest") } -} + // Run the defer with Apollo Server tests only from a specific CI job + val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() + if (runDeferWithApolloServerTests) { + filter.setIncludePatterns("test.DeferWithApolloServerTest") + } else { + filter.setExcludePatterns("test.DeferWithApolloServerTest") + } +} diff --git a/tests/defer/src/commonMain/graphql/base/operation.graphql b/tests/defer/src/commonMain/graphql/base/operation.graphql index fad24d09441..3f6c144b4a2 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 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..b04a788138b --- /dev/null +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -0,0 +1,360 @@ +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.WithFragmentSpreadsMutation +import defer.WithFragmentSpreadsQuery +import defer.WithInlineFragmentsQuery +import defer.fragment.ComputerErrorField +import defer.fragment.ComputerFields +import defer.fragment.FragmentOnQuery +import defer.fragment.ScreenFields +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) + } +} From 981dd464770b5aac3fb9c121e899a1989da07436 Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 17 Dec 2024 15:26:28 +0100 Subject: [PATCH 5/6] Add a few more edge case tests --- .../defer-with-apollo-server-tests.yml | 37 ---- .github/workflows/defer-with-router-tests.yml | 28 +++ tests/defer/build.gradle.kts | 30 +-- .../commonMain/graphql/base/operation.graphql | 43 +++++ .../graphql/noTypename/operation.graphql | 11 ++ .../graphql/noTypename/schema.graphqls | 31 +++ .../kotlin/test/DeferWithApolloServerTest.kt | 176 ++++++++++++++++++ 7 files changed, 306 insertions(+), 50 deletions(-) delete mode 100644 .github/workflows/defer-with-apollo-server-tests.yml create mode 100644 tests/defer/src/commonMain/graphql/noTypename/operation.graphql create mode 100644 tests/defer/src/commonMain/graphql/noTypename/schema.graphqls diff --git a/.github/workflows/defer-with-apollo-server-tests.yml b/.github/workflows/defer-with-apollo-server-tests.yml deleted file mode 100644 index 8397a845919..00000000000 --- a/.github/workflows/defer-with-apollo-server-tests.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: defer-router-tests - -on: - schedule: - - cron: '0 3 * * *' -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - -jobs: - defer-with-router-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/.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/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index 40feb9e2bfb..7d5cf1c0d8f 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -62,6 +62,14 @@ fun configureApollo(generateKotlinModels: Boolean) { } } +apollo { + service("noTypename-$extra") { + packageName.set("defer.notypename") + srcDir("src/commonMain/graphql/noTypename") + addTypename.set("ifPolymorphic") + } +} + configureApollo(true) if (System.getProperty("idea.sync.active") == null) { registerJavaCodegenTestTask() @@ -79,19 +87,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") - } - - // Run the defer with Apollo Server tests only from a specific CI job val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() - if (runDeferWithApolloServerTests) { - filter.setIncludePatterns("test.DeferWithApolloServerTest") - } else { - filter.setExcludePatterns("test.DeferWithApolloServerTest") - } + 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 3f6c144b4a2..e89921fd895 100644 --- a/tests/defer/src/commonMain/graphql/base/operation.graphql +++ b/tests/defer/src/commonMain/graphql/base/operation.graphql @@ -168,3 +168,46 @@ 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 + } + } + } +} 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/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt index b04a788138b..c6ddf98bcdd 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -14,6 +14,9 @@ import defer.DoesNotDisableDeferWithNullIfArgumentQuery import defer.HandlesErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery +import defer.Overlapping2Query +import defer.OverlappingQuery +import defer.SubPathQuery import defer.WithFragmentSpreadsMutation import defer.WithFragmentSpreadsQuery import defer.WithInlineFragmentsQuery @@ -21,6 +24,7 @@ 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 @@ -357,4 +361,176 @@ class DeferWithApolloServerTest { 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) + } + + } From e2da85981d4735fb33f48f5cb75e01db33ee1b62 Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 17 Dec 2024 16:08:43 +0100 Subject: [PATCH 6/6] Fix missed test --- .../src/jvmTest/kotlin/test/DeferJvmTest.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) 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)) + ) + ), ) )