From 96da0994d522845d506f43062c85f9c639adc9af Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 13 Dec 2024 18:30:09 +0100 Subject: [PATCH 1/9] 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 b81f275b8f560c52db0745967b4eb03d45e00730 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 14:44:46 +0100 Subject: [PATCH 2/9] Track pending fragment ids rather than completed ones. --- .../apollo/api/BooleanExpression.kt | 10 +- .../api/android/apollo-runtime.api | 2 +- .../api/apollo-runtime.klib.api | 4 +- .../apollo-runtime/api/jvm/apollo-runtime.api | 2 +- .../apollo/internal/DeferredJsonMerger.kt | 22 +- .../network/http/HttpNetworkTransport.kt | 2 +- .../websocket/WebSocketNetworkTransport.kt | 2 +- .../network/ws/WebSocketNetworkTransport.kt | 3 +- .../test/defer/DeferredJsonMergerTest.kt | 473 +++++++++++++++--- 9 files changed, 435 insertions(+), 85 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 464b4833559..bddc85a8729 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 @@ -74,16 +74,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/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 1eb9c3330bb..42df03b77cf 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -219,7 +219,7 @@ public final class com/apollographql/apollo/internal/DeferredJsonMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getMergedFragmentIds ()Ljava/util/Set; + public final fun getPendingFragmentIds ()Ljava/util/Set; public final fun isEmptyPayload ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index 29a86706313..315cd896802 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -205,8 +205,8 @@ final class com.apollographql.apollo.internal/DeferredJsonMerger { // com.apollo final val merged // com.apollographql.apollo.internal/DeferredJsonMerger.merged|{}merged[0] final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merged.|(){}[0] - final val mergedFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds|{}mergedFragmentIds[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds.|(){}[0] + final val pendingFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds|{}pendingFragmentIds[0] + final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds.|(){}[0] final var hasNext // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext|{}hasNext[0] final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext.|(){}[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index ae89653f943..b0f372bd9de 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -219,7 +219,7 @@ public final class com/apollographql/apollo/internal/DeferredJsonMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getMergedFragmentIds ()Ljava/util/Set; + public final fun getPendingFragmentIds ()Ljava/util/Set; public final fun isEmptyPayload ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; 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 5a2e29cc9b8..2f332ce8a11 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 @@ -222,7 +222,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 43297604af74d2e0852f377a0971d3c34b3d8f4f Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 17:46:57 +0100 Subject: [PATCH 3/9] 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 4cdc188ad285ba82aceab78d0b69d398a49f9193 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 19:41:00 +0100 Subject: [PATCH 4/9] 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 6448f9c36d0..df6652fc118 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -74,5 +74,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 047e6825e8a643058eaad7fa1aa988d719051d0b Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 17 Dec 2024 15:26:28 +0100 Subject: [PATCH 5/9] 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 df6652fc118..d90c4f5fd6d 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -50,6 +50,14 @@ fun configureApollo(generateKotlinModels: Boolean) { } } +apollo { + service("noTypename") { + packageName.set("defer.notypename") + srcDir("src/commonMain/graphql/noTypename") + addTypename.set("ifPolymorphic") + } +} + configureApollo(true) if (System.getProperty("idea.sync.active") == null) { registerJavaCodegenTestTask() @@ -67,19 +75,15 @@ fun com.apollographql.apollo.gradle.api.Service.configureConnection(generateKotl } tasks.withType(AbstractTestTask::class.java) { - // Run the defer with Router tests only from a specific CI job + // Run the defer with Router and defer with Apollo Server tests only from a specific CI job val runDeferWithRouterTests = System.getenv("DEFER_WITH_ROUTER_TESTS").toBoolean() - if (runDeferWithRouterTests) { - filter.setIncludePatterns("test.DeferWithRouterTest") - } else { - filter.setExcludePatterns("test.DeferWithRouterTest") - } - - // 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 6e403249a526edc93e0991b7eb503ec3e923ee7d Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 17 Dec 2024 16:08:43 +0100 Subject: [PATCH 6/9] 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)) + ) + ), ) ) From 4c93fa33ec0d24aad25cf3d0a13a87774c8dbd26 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 18 Jul 2025 15:59:10 +0200 Subject: [PATCH 7/9] Support appending lists in DeferredJsonMerger (for @stream) --- .../apollo/internal/DeferredJsonMerger.kt | 31 +- .../test/defer/DeferredJsonMergerTest.kt | 2980 +++++++++++------ tests/defer/apollo-server/computers.graphqls | 7 + tests/defer/apollo-server/computers.js | 16 +- tests/defer/build.gradle.kts | 2 +- .../commonMain/graphql/base/operation.graphql | 13 + .../commonMain/graphql/base/schema.graphqls | 7 + .../kotlin/test/DeferWithApolloServerTest.kt | 116 + 8 files changed, 2052 insertions(+), 1120 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 c0d0e0d1d09..47a6fdb4204 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 @@ -114,22 +114,35 @@ class DeferredJsonMerger { } 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 id = incrementalItem["id"] as String? ?: error("No id found in incremental result") + val data = incrementalItem["data"] as JsonMap? + val items = incrementalItem["items"] as List? val subPath = incrementalItem["subPath"] as List? ?: emptyList() 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) + val nodeToMergeInto = nodeAtPath(mergedData, path) + when { + data != null -> { + deepMergeObject(nodeToMergeInto as MutableJsonMap, data) + } + + items != null -> { + mergeList(nodeToMergeInto as MutableList, items) + } + + else -> { + error("Neither data nor items found in incremental result") + } + } } - private fun deepMerge(destination: MutableJsonMap, map: JsonMap) { - for ((key, value) in map) { + private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { + for ((key, value) in obj) { if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { // Objects: merge recursively val fieldDestination = destination[key] as MutableJsonMap val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") - deepMerge(destination = fieldDestination, map = fieldMap) + deepMergeObject(destination = fieldDestination, obj = fieldMap) } else { // Other types: add / overwrite destination[key] = value @@ -137,6 +150,10 @@ class DeferredJsonMerger { } } + private fun mergeList(destination: MutableList, items: List) { + destination.addAll(items) + } + private fun jsonToMap(json: BufferedSource): JsonMap = BufferedSourceJsonReader(json).readAny() as JsonMap 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 c0a9c4cf480..f1c189ad919 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt @@ -1,5 +1,8 @@ +@file:OptIn(ApolloInternal::class) + package test.defer +import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.json.BufferedSourceJsonReader import com.apollographql.apollo.api.json.readAny @@ -22,57 +25,57 @@ class DeferredJsonMergerTest { //language=JSON val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true } - ] - }, - "pending": [ + }, { - "id": "0", - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0" + "id": "Computer2", + "screen": { + "isTouch": false + } } - ], - "hasNext": true - } - """ + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true } - ] - } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -84,73 +87,73 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "id": "0" - } - ], - "completed": [ - { - "id": "0" - } - ], - "pending": [ - { - "id": "1", - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0" - } - ], - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - }, - "hasNext": true - } - """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + } + ], + "completed": [ + { + "id": "0" + } + ], + "pending": [ + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false } - ] - }, - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" } + ] + }, + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" } - } - """ + } + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -162,77 +165,77 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "id": "1" - } - ], - "completed": [ - { - "id": "1" - } - ], - "pending": [ - { - "id": "2", - "path": [ - "computers", - 0, - "screen" - ], - "label": "fragment:ComputerFields:0" - } - ], - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } - }, - "hasNext": true - } - """ + { + "incremental": [ + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } + }, + "id": "1" + } + ], + "completed": [ + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" } - ] - }, - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" } + ] + }, + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -244,93 +247,93 @@ class DeferredJsonMergerTest { //language=JSON val payload4 = """ - { - "completed": [ + { + "completed": [ + { + "id": "2", + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ] + } + ], + "pending": [ + { + "id": "3", + "path": [ + "computers", + 1, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4 = """ + { + "data": { + "computers": [ { - "id": "2", - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ] - } - ], - "pending": [ + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, { - "id": "3", - "path": [ - "computers", - 1, - "screen" - ], - "label": "fragment:ComputerFields:0" + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } } - ], - "hasNext": true - } - """ - //language=JSON - val mergedPayloads_1_2_3_4 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, + ] + }, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } + "line": 12, + "column": 11 } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" ] - }, - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload4.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) assertEquals( @@ -343,102 +346,102 @@ class DeferredJsonMergerTest { //language=JSON val payload5 = """ - { - "incremental": [ - { - "data": { - "isColor": false - }, - "id": "3", - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ] - } - ], - "completed": [ - { - "id": "3" - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - }, - "hasNext": false - } - """ - //language=JSON - val mergedPayloads_1_2_3_4_5 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", + "errors": [ { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480", - "isColor": false - } + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] } ] - }, - "errors": [ + } + ], + "completed": [ + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } }, { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } } + ] + }, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + }, + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload5.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) assertEquals( @@ -455,65 +458,65 @@ class DeferredJsonMergerTest { //language=JSON val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "pending": [ + { + "data": { + "computers": [ { - "id": "0", - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0" + "id": "Computer1", + "screen": { + "isTouch": true + } }, { - "id": "1", - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0" + "id": "Computer2", + "screen": { + "isTouch": false + } } - ], - "hasNext": true - } - """ + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true } - ] - } - } - """ + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + } + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -526,99 +529,99 @@ class DeferredJsonMergerTest { //language=JSON val payload2_3 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "id": "0" - }, - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "id": "1" - } - ], - "completed": [ - { - "id": "0" + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } }, - { - "id": "1" - } - ], - "pending": [ - { - "id": "2", - "path": [ - "computers", - 0, - "screen" - ], - "label": "fragment:ComputerFields:0" + "id": "0" + }, + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } }, - { - "id": "3", - "path": [ - "computers", - 1, - "screen" - ], - "label": "fragment:ComputerFields:0" - } - ], - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } + "id": "1" + } + ], + "completed": [ + { + "id": "0" }, - "hasNext": true - } - """ + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + }, + { + "id": "3", + "path": [ + "computers", + 1, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } + { + "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" } + ] + }, + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2_3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -631,122 +634,122 @@ class DeferredJsonMergerTest { //language=JSON val payload4_5 = """ - { - "incremental": [ - { - "data": { - "isColor": false - }, - "id": "3", - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ] - } - ], - "completed": [ - { - "id": "2", - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ] + { + "incremental": [ + { + "data": { + "isColor": false }, - { - "id": "3" - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - }, - "hasNext": false - } - """ - //language=JSON - val mergedPayloads_1_2_3_4_5 = """ - { - "data": { - "computers": [ + "id": "3", + "errors": [ { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ] + } + ], + "completed": [ + { + "id": "2", + "errors": [ { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480", - "isColor": false - } + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] } ] }, - "errors": [ + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } }, { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } } + ] + }, + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + }, + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload4_5.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) assertEquals( @@ -763,82 +766,82 @@ class DeferredJsonMergerTest { //language=JSON val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "pending": [ + { + "data": { + "computers": [ { - "id": "0", - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0" + "id": "Computer1", + "screen": { + "isTouch": true + } }, { - "id": "1", - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0" + "id": "Computer2", + "screen": { + "isTouch": false + } } - ], - "hasNext": true - } - """ + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertFalse(deferredJsonMerger.isEmptyPayload) //language=JSON val payload2 = """ - { - "hasNext": true - } - """ + { + "hasNext": true + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertTrue(deferredJsonMerger.isEmptyPayload) //language=JSON val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "id": "0" - } - ], - "hasNext": true - } - """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertFalse(deferredJsonMerger.isEmptyPayload) //language=JSON val payload4 = """ - { - "hasNext": false - } - """ + { + "hasNext": false + } + """.trimIndent() deferredJsonMerger.merge(payload4.buffer()) assertTrue(deferredJsonMerger.isEmptyPayload) } @@ -851,38 +854,49 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "f2": { - "a": "a", - "b": "b", - "c": { - "d": "d", - "e": "e", - "f": { "h": "h", "i": "i" } + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i" } } - }, - "pending": [{ "path": [], "id": "0" }], - "hasNext": true - } - """ + } + }, + "pending": [ + { + "path": [], + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "f2": { - "a": "a", - "b": "b", - "c": { - "d": "d", - "e": "e", - "f": { "h": "h", "i": "i" } + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -894,32 +908,55 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { "id": "0", "data": { "MyFragment": "Query" } }, - { "id": "0", "subPath": ["f2", "c", "f"], "data": { "j": "j" } } - ], - "completed": [{ "id": "0" }], - "hasNext": false - } - """ + { + "incremental": [ + { + "id": "0", + "data": { + "MyFragment": "Query" + } + }, + { + "id": "0", + "subPath": [ + "f2", + "c", + "f" + ], + "data": { + "j": "j" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "f2": { - "a": "a", - "b": "b", - "c": { - "d": "d", - "e": "e", - "f": { "h": "h", "i": "i", "j": "j" } + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i", + "j": "j" } - }, - "MyFragment": "Query" - } + } + }, + "MyFragment": "Query" } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -936,35 +973,50 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": {"f2": {"a": "A", "b": "B", "c": { - "d": "D", "e": "E", "f": { - "h": "H", "i": "I" + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" + } } - }}}, - "pending": [{"id": "0", "path": [], "label": "D1"}], - "hasNext": true - } - """ + } + }, + "pending": [ + { + "id": "0", + "path": [], + "label": "D1" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "f2": { - "a": "A", - "b": "B", - "c": { - "d": "D", - "e": "E", - "f": { - "h": "H", - "i": "I" - } + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -976,38 +1028,61 @@ class DeferredJsonMergerTest { //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 - } - """ + { + "incremental": [ + { + "id": "0", + "subPath": [ + "f2", + "c", + "f" + ], + "data": { + "j": "J", + "k": "K" + } + } + ], + "pending": [ + { + "id": "1", + "path": [ + "f2", + "c", + "f" + ], + "label": "D2" + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "f2": { - "a": "A", - "b": "B", - "c": { - "d": "D", - "e": "E", - "f": { - "h": "H", - "i": "I", - "j": "J", - "k": "K" - } + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1019,40 +1094,48 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - {"id": "1", "data": {"l": "L", "m": "M"}} - ], - "completed": [ - {"id": "1"} - ], - "hasNext": false - } - """ + { + "incremental": [ + { + "id": "1", + "data": { + "l": "L", + "m": "M" + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "f2": { - "a": "A", - "b": "B", - "c": { - "d": "D", - "e": "E", - "f": { - "h": "H", - "i": "I", - "j": "J", - "k": "K", - "l": "L", - "m": "M" - } + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K", + "l": "L", + "m": "M" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1069,32 +1152,49 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "a": { "b": { "c": { "d": "d" } } } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + }, + "pending": [ + { + "path": [], + "id": "0", + "label": "Blue" }, - "pending": [ - { "path": [], "id": "0", "label": "Blue" }, - { "path": ["a", "b"], "id": "1", "label": "Red" } - ], - "hasNext": true - } - """ + { + "path": [ + "a", + "b" + ], + "id": "1", + "label": "Red" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1107,33 +1207,49 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } }, - { "id": "1", "data": { "e": { "f": "f" } } } - ], - "completed": [{ "id": "1" }], - "hasNext": true - } - """ + { + "incremental": [ + { + "id": "1", + "data": { + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + { + "id": "1", + "data": { + "e": { + "f": "f" + } + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - }, - "e": { - "f": "f" - }, - "potentiallySlowFieldA": "potentiallySlowFieldA" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1145,36 +1261,48 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } } - ], - "completed": [{ "id": "0" }], - "hasNext": false - } - """ + { + "incremental": [ + { + "id": "0", + "data": { + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - }, - "e": { - "f": "f" - }, - "potentiallySlowFieldA": "potentiallySlowFieldA" - } - }, - "g": { - "h": "h" - }, - "potentiallySlowFieldB": "potentiallySlowFieldB" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1191,32 +1319,49 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "a": { "b": { "c": { "d": "d" } } } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + }, + "pending": [ + { + "path": [], + "id": "0", + "label": "Blue" }, - "pending": [ - { "path": [], "id": "0", "label": "Blue" }, - { "path": ["a", "b"], "id": "1", "label": "Red" } - ], - "hasNext": true - } - """ + { + "path": [ + "a", + "b" + ], + "id": "1", + "label": "Red" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1229,36 +1374,55 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } }, - { "id": "1", "data": { "e": { "f": "f" } } } - ], - "completed": [{ "id": "0" }], - "hasNext": true - } - """ + { + "incremental": [ + { + "id": "0", + "data": { + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + }, + { + "id": "1", + "data": { + "e": { + "f": "f" + } + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - }, - "e": { - "f": "f" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" } - }, - "g": { - "h": "h" - }, - "potentiallySlowFieldB": "potentiallySlowFieldB" - } + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1272,13 +1436,21 @@ class DeferredJsonMergerTest { val payload3 = """ { "incremental": [ - { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } } + { + "id": "1", + "data": { + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + } + ], + "completed": [ + { + "id": "1" + } ], - "completed": [{ "id": "1" }], "hasNext": false } - """ - //language=JSON + """.trimIndent() val mergedPayloads_1_2_3 = """ { "data": { @@ -1316,23 +1488,33 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { "me": {} }, - "pending": [ - { "path": [], "id": "0" }, - { "path": ["me"], "id": "1" } - ], - "hasNext": true - } - """ + { + "data": { + "me": {} + }, + "pending": [ + { + "path": [], + "id": "0" + }, + { + "path": [ + "me" + ], + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": {} - } + { + "data": { + "me": {} } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1345,34 +1527,92 @@ class DeferredJsonMergerTest { //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": { + { + "incremental": [ + { + "id": "1", + "data": { "list": [ - { "item": { "id": "1" } }, - { "item": { "id": "2" } }, - { "item": { "id": "3" } } + { + "item": {} + }, + { + "item": {} + }, + { + "item": {} + } ] } + }, + { + "id": "1", + "subPath": [ + "list", + 0, + "item" + ], + "data": { + "id": "1" + } + }, + { + "id": "1", + "subPath": [ + "list", + 1, + "item" + ], + "data": { + "id": "2" + } + }, + { + "id": "1", + "subPath": [ + "list", + 2, + "item" + ], + "data": { + "id": "3" + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "list": [ + { + "item": { + "id": "1" + } + }, + { + "item": { + "id": "2" + } + }, + { + "item": { + "id": "3" + } + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1384,30 +1624,82 @@ class DeferredJsonMergerTest { //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 - } - """ + { + "incremental": [ + { + "id": "0", + "subPath": [ + "me", + "list", + 0, + "item" + ], + "data": { + "value": "Foo" + } + }, + { + "id": "0", + "subPath": [ + "me", + "list", + 1, + "item" + ], + "data": { + "value": "Bar" + } + }, + { + "id": "0", + "subPath": [ + "me", + "list", + 2, + "item" + ], + "data": { + "value": "Baz" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "me": { - "list": [ - { "item": { "id": "1", "value": "Foo" } }, - { "item": { "id": "2", "value": "Bar" } }, - { "item": { "id": "3", "value": "Baz" } } - ] - } + { + "data": { + "me": { + "list": [ + { + "item": { + "id": "1", + "value": "Foo" + } + }, + { + "item": { + "id": "2", + "value": "Bar" + } + }, + { + "item": { + "id": "3", + "value": "Baz" + } + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1424,24 +1716,30 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "me": {} - }, - "pending": [ - {"id": "0", "path": ["me"], "label": "B"} - ], - "hasNext": true - } - """ + { + "data": { + "me": {} + }, + "pending": [ + { + "id": "0", + "path": [ + "me" + ], + "label": "B" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": {} - } + { + "data": { + "me": {} } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1453,27 +1751,35 @@ class DeferredJsonMergerTest { //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": { + { + "incremental": [ + { + "id": "0", + "data": { "a": "A", "b": "B" } } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "a": "A", + "b": "B" + } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1490,33 +1796,53 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "me": { - "id": 1, - "avatarUrl": "http://…", - "projects": [{ "name": "My Project" }] - } + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ] + } + }, + "pending": [ + { + "id": "0", + "path": [ + "me" + ], + "label": "Billing" }, - "pending": [ - { "id": "0", "path": ["me"], "label": "Billing" }, - { "id": "1", "path": ["me"], "label": "Prev" } - ], - "hasNext": true - } - """ + { + "id": "1", + "path": [ + "me" + ], + "label": "Prev" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": { - "id": 1, - "avatarUrl": "http://…", - "projects": [{ "name": "My Project" }] - } + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1529,36 +1855,44 @@ class DeferredJsonMergerTest { //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" }], + { + "incremental": [ + { + "id": "0", + "data": { "tier": "BRONZE", "renewalDate": "2023-03-20", "latestInvoiceTotal": "${'$'}12.34" } } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34" + } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1570,33 +1904,51 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - { - "id": "1", - "data": { "previousInvoices": [{ "name": "My Invoice" }] } + { + "incremental": [ + { + "id": "1", + "data": { + "previousInvoices": [ + { + "name": "My Invoice" + } + ] } - ], - "completed": [{ "id": "1" }], - "hasNext": false - } - """ + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "me": { - "id": 1, - "avatarUrl": "http://…", - "projects": [{ "name": "My Project" }], - "tier": "BRONZE", - "renewalDate": "2023-03-20", - "latestInvoiceTotal": "${'$'}12.34", - "previousInvoices": [{ "name": "My Invoice" }] - } + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34", + "previousInvoices": [ + { + "name": "My Invoice" + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1613,25 +1965,35 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "me": {} + { + "data": { + "me": {} + }, + "pending": [ + { + "id": "0", + "path": [], + "label": "A" }, - "pending": [ - {"id": "0", "path": [], "label": "A"}, - {"id": "1", "path": ["me"], "label": "B"} - ], - "hasNext": true - } - """ + { + "id": "1", + "path": [ + "me" + ], + "label": "B" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": {} - } + { + "data": { + "me": {} } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1644,41 +2006,53 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { - "id": "0", - "subPath": ["me"], - "data": { "foo": { "bar": {} } } - }, - { - "id": "0", - "subPath": ["me", "foo", "bar"], - "data": { - "baz": "BAZ" + { + "incremental": [ + { + "id": "0", + "subPath": [ + "me" + ], + "data": { + "foo": { + "bar": {} } } - ], - "completed": [ - {"id": "0"} - ], - "hasNext": true - } - """ + }, + { + "id": "0", + "subPath": [ + "me", + "foo", + "bar" + ], + "data": { + "baz": "BAZ" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "me": { - "foo": { - "bar": { - "baz": "BAZ" - } + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1690,58 +2064,444 @@ class DeferredJsonMergerTest { //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 - } - """ + { + "completed": [ + { + "id": "1", + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": [ + "foo", + "bar", + "qux" + ] + } + ] + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "me": { - "foo": { - "bar": { - "baz": "BAZ" - } + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" } } + } + }, + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": [ + "foo", + "bar", + "qux" + ] + } + ] + } + """.trimIndent() + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example I from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleI() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + }, + "pending": [ + { + "id": "0", + "path": [ + "person" + ], + "label": "homeWorldDefer" }, - "errors": [ - { - "message": "Cannot return null for non-nullable field Bar.qux.", - "locations": [ - { - "line": 1, - "column": 1 - } - ], - "path": ["foo", "bar", "qux"] + { + "id": "1", + "path": [ + "person", + "films" + ], + "label": "filmsStream" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "items": [ + { + "title": "Return of the Jedi" + } + ] + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } + } + } + """.trimIndent() + + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload4 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "homeworld": { + "name": "Tatooine" + } } - ] + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "homeworld": { + "name": "Tatooine" + }, + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } } - """ + } + """.trimIndent() + + deferredJsonMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) + assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example J from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleJ() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + } + ] + } + }, + "pending": [ + { + "id": "1", + "path": [ + "person", + "films" + ], + "label": "filmsStream" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "items": [ + { + "title": "The Empire Strikes Back" + } + ] + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1", + "errors": [ + { + "message": "Cannot return null for non-nullable field Person.films.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "person", + "films" + ] + } + ] + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + }, + "errors": [ + { + "message": "Cannot return null for non-nullable field Person.films.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "person", + "films" + ] + } + ] + } + """.trimIndent() + deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), deferredJsonMerger.pendingFragmentIds ) diff --git a/tests/defer/apollo-server/computers.graphqls b/tests/defer/apollo-server/computers.graphqls index 4c410b7fe7e..a1875342b39 100644 --- a/tests/defer/apollo-server/computers.graphqls +++ b/tests/defer/apollo-server/computers.graphqls @@ -14,6 +14,7 @@ type Computer { screen: Screen! errorField: String nonNullErrorField: String! + peripherals: [String!]! } type Screen { @@ -25,3 +26,9 @@ directive @defer( if: Boolean! = true label: String ) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD diff --git a/tests/defer/apollo-server/computers.js b/tests/defer/apollo-server/computers.js index d5cedcd16b0..77e673b7985 100644 --- a/tests/defer/apollo-server/computers.js +++ b/tests/defer/apollo-server/computers.js @@ -5,8 +5,20 @@ 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}}, + { + id: 'Computer1', + cpu: "386", + year: 1993, + screen: {resolution: "640x480", isColor: false}, + peripherals: ["Keyboard", "Mouse", "Printer"], + }, + { + id: 'Computer2', + cpu: "486", + year: 1996, + screen: {resolution: "800x600", isColor: true}, + peripherals: ["Keyboard", "Mouse", "Printer", "Scanner"], + }, ] const typeDefs = readFileSync('./computers.graphqls', {encoding: 'utf-8'}); diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index d90c4f5fd6d..0f1b0ea79c7 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -77,7 +77,7 @@ fun com.apollographql.apollo.gradle.api.Service.configureConnection(generateKotl tasks.withType(AbstractTestTask::class.java) { // 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() - val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() + val runDeferWithApolloServerTests = true filter.setIncludePatterns(*buildList { if (runDeferWithRouterTests) add("test.DeferWithRouterTest") if (runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest") diff --git a/tests/defer/src/commonMain/graphql/base/operation.graphql b/tests/defer/src/commonMain/graphql/base/operation.graphql index e89921fd895..c3b9b8d57b9 100644 --- a/tests/defer/src/commonMain/graphql/base/operation.graphql +++ b/tests/defer/src/commonMain/graphql/base/operation.graphql @@ -211,3 +211,16 @@ query SubPathQuery { } } } + +query SimpleStreamQuery($initialCount: Int!) { + computers @stream(initialCount: $initialCount) { + id + } +} + +query NestedStreamQuery($initialCount: Int!) { + computers @stream(initialCount: $initialCount) { + id + peripherals @stream(initialCount: $initialCount) + } +} diff --git a/tests/defer/src/commonMain/graphql/base/schema.graphqls b/tests/defer/src/commonMain/graphql/base/schema.graphqls index 0892f0f4075..9de38fc9a65 100644 --- a/tests/defer/src/commonMain/graphql/base/schema.graphqls +++ b/tests/defer/src/commonMain/graphql/base/schema.graphqls @@ -23,9 +23,16 @@ type Computer { screen: Screen! errorField: String nonNullErrorField: String! + peripherals: [String!]! } type Screen { resolution: String! isColor: Boolean! } + +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD diff --git a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt index c6ddf98bcdd..1746e941c95 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -14,8 +14,10 @@ import defer.DoesNotDisableDeferWithNullIfArgumentQuery import defer.HandlesErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery +import defer.NestedStreamQuery import defer.Overlapping2Query import defer.OverlappingQuery +import defer.SimpleStreamQuery import defer.SubPathQuery import defer.WithFragmentSpreadsMutation import defer.WithFragmentSpreadsQuery @@ -532,5 +534,119 @@ class DeferWithApolloServerTest { assertResponseListEquals(expectedDataList, actualResponseList) } + @Test + fun simpleStream0Initial() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[]},"pending":[{"id":"0","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"id":"0","items":[{"id":"Computer1"},{"id":"Computer2"}]}],"completed":[{"id":"0"}]} + val query = SimpleStreamQuery(0) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf() + ) + ) + .build(), + + + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + SimpleStreamQuery.Computer("Computer2"), + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun simpleStream1Initial() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"id":"Computer1"}]},"pending":[{"id":"0","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"id":"0","items":[{"id":"Computer2"}]}],"completed":[{"id":"0"}]} + val query = SimpleStreamQuery(1) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + ) + ) + ) + .build(), + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + SimpleStreamQuery.Computer("Computer2"), + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun nestedStream() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"id":"Computer1","peripherals":["Keyboard"]}]},"pending":[{"id":"0","path":["computers",0,"peripherals"]},{"id":"1","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",1,"peripherals"]}],"incremental":[{"id":"0","items":["Mouse","Printer"]},{"id":"1","items":[{"id":"Computer2","peripherals":["Keyboard"]}]},{"id":"2","items":["Mouse","Printer","Scanner"]}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"}]} + val query = NestedStreamQuery(1) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + NestedStreamQuery.Data( + listOf( + NestedStreamQuery.Computer("Computer1", listOf("Keyboard")) + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + NestedStreamQuery.Data( + listOf( + NestedStreamQuery.Computer("Computer1", listOf("Keyboard", "Mouse", "Printer")), + NestedStreamQuery.Computer("Computer2", listOf("Keyboard", "Mouse", "Printer", "Scanner")) + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } } From 4fa99adf3bca3eb05826d53574ed0f0052b5a1a2 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 18 Jul 2025 18:24:58 +0200 Subject: [PATCH 8/9] Rename DeferredJsonMerger -> IncrementalResultsMerger and DeferredFragmentIdentifier -> IncrementalResultIdentifier --- libraries/apollo-api/api/apollo-api.api | 1 + libraries/apollo-api/api/apollo-api.klib.api | 1 + .../apollo/api/BooleanExpression.kt | 13 +- .../apollo/api/CustomScalarAdapters.kt | 19 +- .../apollographql/apollo/api/Executables.kt | 8 +- ...fier.kt => IncrementalResultIdentifier.kt} | 6 + .../apollographql/apollo/api/Operations.kt | 8 +- .../apollo/api/internal/ResponseParser.kt | 12 +- .../api/android/apollo-runtime.api | 6 +- .../api/apollo-runtime.klib.api | 32 +- .../apollo-runtime/api/jvm/apollo-runtime.api | 6 +- .../apollo/internal/DeferredJsonMerger.kt | 187 ---------- .../internal/IncrementalResultsMerger.kt | 186 ++++++++++ .../network/http/HttpNetworkTransport.kt | 19 +- .../websocket/WebSocketNetworkTransport.kt | 14 +- .../network/ws/WebSocketNetworkTransport.kt | 18 +- ...est.kt => IncrementalResultsMergerTest.kt} | 348 +++++++++--------- 17 files changed, 449 insertions(+), 435 deletions(-) rename libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/{DeferredFragmentIdentifier.kt => IncrementalResultIdentifier.kt} (54%) delete mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt create mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt rename libraries/apollo-runtime/src/commonTest/kotlin/test/defer/{DeferredJsonMergerTest.kt => IncrementalResultsMergerTest.kt} (78%) diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index 2654939090c..4fffa343371 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -426,6 +426,7 @@ public final class com/apollographql/apollo/api/CustomScalarAdapters$Builder { public final fun deferredFragmentIdentifiers (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun errors (Ljava/util/List;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun falseVariables (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; + public final fun pendingResultIds (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; } public final class com/apollographql/apollo/api/CustomScalarAdapters$Key : com/apollographql/apollo/api/ExecutionContext$Key { diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index 15357f92a74..ff12ab2dbf2 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -938,6 +938,7 @@ final class com.apollographql.apollo.api/CustomScalarAdapters : com.apollographq final fun deferredFragmentIdentifiers(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.deferredFragmentIdentifiers|deferredFragmentIdentifiers(kotlin.collections.Set?){}[0] final fun errors(kotlin.collections/List?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.errors|errors(kotlin.collections.List?){}[0] final fun falseVariables(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.falseVariables|falseVariables(kotlin.collections.Set?){}[0] + final fun pendingResultIds(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.pendingResultIds|pendingResultIds(kotlin.collections.Set?){}[0] } final object Key : com.apollographql.apollo.api/ExecutionContext.Key { // com.apollographql.apollo.api/CustomScalarAdapters.Key|null[0] diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt index bddc85a8729..cfe2db00c53 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt @@ -50,7 +50,8 @@ fun and(vararg other: BooleanExpression): BooleanExpression = Bo fun not(other: BooleanExpression): BooleanExpression = BooleanExpression.Not(other) fun variable(name: String): BooleanExpression = BooleanExpression.Element(BVariable(name)) fun label(label: String? = null): BooleanExpression = BooleanExpression.Element(BLabel(label)) -fun possibleTypes(vararg typenames: String): BooleanExpression = BooleanExpression.Element(BPossibleTypes(typenames.toSet())) +fun possibleTypes(vararg typenames: String): BooleanExpression = + BooleanExpression.Element(BPossibleTypes(typenames.toSet())) internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boolean { return when (this) { @@ -66,7 +67,7 @@ internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boo fun BooleanExpression.evaluate( variables: Set?, typename: String?, - deferredFragmentIdentifiers: Set?, + pendingResultIds: Set?, path: List?, ): Boolean { // Remove "data" from the path @@ -74,22 +75,22 @@ fun BooleanExpression.evaluate( return evaluate { when (it) { is BVariable -> !(variables?.contains(it.name) ?: false) - is BLabel -> !isDeferredFragmentPending(deferredFragmentIdentifiers, croppedPath!!, it.label) + is BLabel -> !isDeferredFragmentPending(pendingResultIds, croppedPath!!, it.label) is BPossibleTypes -> it.possibleTypes.contains(typename) } } } private fun isDeferredFragmentPending( - deferredFragmentIdentifiers: Set?, + pendingResultIds: Set?, path: List, label: String?, ): Boolean { - if (deferredFragmentIdentifiers == null) { + if (pendingResultIds == null) { // By default, parse all deferred fragments - this is the case when parsing from the normalized cache. return false } - return deferredFragmentIdentifiers.contains(DeferredFragmentIdentifier(path, label)) + return pendingResultIds.contains(IncrementalResultIdentifier(path, label)) } /** diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt index a02faf1ac44..585aa94a4c3 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt @@ -19,10 +19,10 @@ class CustomScalarAdapters private constructor( @JvmField val falseVariables: Set?, /** - * Defer identifiers used to determine whether the parser must parse @defer fragments + * Pending incremental result identifiers used to determine whether the parser must parse deferred fragments */ @JvmField - val deferredFragmentIdentifiers: Set?, + val deferredFragmentIdentifiers: Set?, /** * Errors to use with @catch */ @@ -125,21 +125,26 @@ class CustomScalarAdapters private constructor( fun newBuilder(): Builder { return Builder().addAll(this) .falseVariables(falseVariables) - .deferredFragmentIdentifiers(deferredFragmentIdentifiers) + .pendingResultIds(deferredFragmentIdentifiers) } class Builder { private val adaptersMap: MutableMap> = mutableMapOf() private var falseVariables: Set? = null - private var deferredFragmentIdentifiers: Set? = null + private var pendingResultIds: Set? = null private var errors: List? = null fun falseVariables(falseVariables: Set?) = apply { this.falseVariables = falseVariables } - fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { - this.deferredFragmentIdentifiers = deferredFragmentIdentifiers + @Deprecated("Use pendingResultIds instead", ReplaceWith("pendingResultIds(pendingResultIds = deferredFragmentIdentifiers)")) + fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { + this.pendingResultIds = deferredFragmentIdentifiers + } + + fun pendingResultIds(pendingResultIds: Set?) = apply { + this.pendingResultIds = pendingResultIds } fun errors(errors: List?) = apply { @@ -173,7 +178,7 @@ class CustomScalarAdapters private constructor( return CustomScalarAdapters( adaptersMap, falseVariables, - deferredFragmentIdentifiers, + pendingResultIds, errors, ) } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt index 28d0fe1c090..59fda6ca3ef 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt @@ -71,12 +71,12 @@ fun Executable.parseData( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, falseVariables: Set? = null, - deferredFragmentIds: Set? = null, - errors: List? = null + deferredFragmentIds: Set? = null, + errors: List? = null, ): D? { val customScalarAdapters1 = customScalarAdapters.newBuilder() .falseVariables(falseVariables) - .deferredFragmentIdentifiers(deferredFragmentIds) + .pendingResultIds(pendingResultIds = deferredFragmentIds) .errors(errors) .build() return adapter().nullable().fromJson(jsonReader, customScalarAdapters1) @@ -89,4 +89,4 @@ fun Executable.composeData( value: D ) { adapter().toJson(jsonWriter, customScalarAdapters, value) -} \ No newline at end of file +} diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt similarity index 54% rename from libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt rename to libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt index 13f5e2d4dac..0d3d6ec5d6a 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt @@ -7,3 +7,9 @@ data class DeferredFragmentIdentifier( val path: List, val label: String?, ) + +/** + * Identifies an incremental result. + * [DeferredFragmentIdentifier] is kept to not break the API/ABI, but this alias is more descriptive of its purpose. + */ +typealias IncrementalResultIdentifier = DeferredFragmentIdentifier diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt index 9dc586f8147..a97159aa4a0 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt @@ -70,7 +70,7 @@ fun Operation.composeJsonRequest( fun Operation.parseJsonResponse( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return jsonReader.use { ResponseParser.parse( @@ -103,7 +103,7 @@ fun Operation.parseResponse( jsonReader: JsonReader, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( @@ -177,7 +177,7 @@ fun JsonReader.toApolloResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return use { try { @@ -213,7 +213,7 @@ fun JsonReader.parseResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt index 2ce1c62f70f..fc611c83e17 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt @@ -3,8 +3,8 @@ package com.apollographql.apollo.api.internal import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters -import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.falseVariables import com.apollographql.apollo.api.json.JsonReader @@ -24,7 +24,7 @@ internal object ResponseParser { operation: Operation, requestUuid: Uuid?, customScalarAdapters: CustomScalarAdapters, - deferredFragmentIds: Set?, + pendingResultIds: Set?, ): ApolloResponse { jsonReader.beginObject() @@ -36,8 +36,9 @@ internal object ResponseParser { when (val name = jsonReader.nextName()) { "data" -> { val falseVariables = operation.falseVariables(customScalarAdapters) - data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, deferredFragmentIds, errors) + data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, pendingResultIds, errors) } + "errors" -> errors = jsonReader.readErrors() "extensions" -> extensions = jsonReader.readAny() as? Map else -> { @@ -100,7 +101,8 @@ private fun JsonReader.readError(): Error { @Suppress("DEPRECATION") - return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields).build() + return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields) + .build() } private fun JsonReader.readPath(): List? { @@ -164,4 +166,4 @@ fun JsonReader.readErrors(): List { } endArray() return list -} \ No newline at end of file +} diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 42df03b77cf..d4e65147106 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/DeferredJsonMerger { +public final class com/apollographql/apollo/internal/IncrementalResultsMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getPendingFragmentIds ()Ljava/util/Set; - public final fun isEmptyPayload ()Z + public final fun getPendingResultIds ()Ljava/util/Set; + public final fun isEmptyResponse ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; public final fun reset ()V diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index 315cd896802..e82fb4ac562 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -200,22 +200,22 @@ final class com.apollographql.apollo.interceptor/AutoPersistedQueryInterceptor : } } -final class com.apollographql.apollo.internal/DeferredJsonMerger { // com.apollographql.apollo.internal/DeferredJsonMerger|null[0] - constructor () // com.apollographql.apollo.internal/DeferredJsonMerger.|(){}[0] - - final val merged // com.apollographql.apollo.internal/DeferredJsonMerger.merged|{}merged[0] - final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merged.|(){}[0] - final val pendingFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds|{}pendingFragmentIds[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds.|(){}[0] - - final var hasNext // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext|{}hasNext[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext.|(){}[0] - final var isEmptyPayload // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload|{}isEmptyPayload[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload.|(){}[0] - - final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(kotlin.collections.Map){}[0] - final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(okio.BufferedSource){}[0] - final fun reset() // com.apollographql.apollo.internal/DeferredJsonMerger.reset|reset(){}[0] +final class com.apollographql.apollo.internal/IncrementalResultsMerger { // com.apollographql.apollo.internal/IncrementalResultsMerger|null[0] + constructor () // com.apollographql.apollo.internal/IncrementalResultsMerger.|(){}[0] + + final val merged // com.apollographql.apollo.internal/IncrementalResultsMerger.merged|{}merged[0] + final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merged.|(){}[0] + final val pendingResultIds // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds|{}pendingResultIds[0] + final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds.|(){}[0] + + final var hasNext // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext|{}hasNext[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext.|(){}[0] + final var isEmptyResponse // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse.|(){}[0] + + final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] + final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] + final fun reset() // com.apollographql.apollo.internal/IncrementalResultsMerger.reset|reset(){}[0] } final class com.apollographql.apollo.internal/MultipartReader : okio/Closeable { // com.apollographql.apollo.internal/MultipartReader|null[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index b0f372bd9de..8806ffc7efd 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/DeferredJsonMerger { +public final class com/apollographql/apollo/internal/IncrementalResultsMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getPendingFragmentIds ()Ljava/util/Set; - public final fun isEmptyPayload ()Z + public final fun getPendingResultIds ()Ljava/util/Set; + public final fun isEmptyResponse ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; public final fun reset ()V diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt deleted file mode 100644 index 47a6fdb4204..00000000000 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.apollographql.apollo.internal - -import com.apollographql.apollo.annotations.ApolloInternal -import com.apollographql.apollo.api.DeferredFragmentIdentifier -import com.apollographql.apollo.api.json.BufferedSourceJsonReader -import com.apollographql.apollo.api.json.readAny -import okio.BufferedSource - -private typealias JsonMap = Map -private typealias MutableJsonMap = MutableMap - -/** - * Utility class for merging GraphQL JSON payloads received in multiple chunks when using the `@defer` directive. - * - * Each call to [merge] will merge the given chunk into the [merged] Map, and will also update the [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 - * [merge], the payload is copied to [merged] as-is). - * - * `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 _pendingFragmentIds = mutableMapOf() - val pendingFragmentIds: Set get() = _pendingFragmentIds.values.toSet() - - var hasNext: Boolean = true - private set - - /** - * A payload can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last - * emitted payload. This field allows to test for this in order to ignore such payloads. - * See https://github.com/apollographql/router/issues/1687. - */ - var isEmptyPayload: Boolean = false - private set - - fun merge(payload: BufferedSource): JsonMap { - val payloadMap = jsonToMap(payload) - return merge(payloadMap) - } - - 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(completed) - return merged - } - handlePending(payload) - - val incrementalList = payload["incremental"] as? List - 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(completed) - - (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } - - return merged - } - - private fun getOrPutMergedErrors() = _merged.getOrPut("errors") { mutableListOf() } as MutableList - - private fun getOrPutExtensions() = _merged.getOrPut("extensions") { mutableMapOf() } as MutableJsonMap - - private fun handlePending(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? - _pendingFragmentIds[id] = DeferredFragmentIdentifier(path = path, label = label) - } - } - } - - private fun handleCompleted(completed: 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) { - getOrPutMergedErrors() += errors - } else { - // Fragment is no longer pending - only if there were no errors - val id = completedItem["id"] as String - _pendingFragmentIds.remove(id) ?: error("Id '$id' not found in pending results") - } - } - } - } - - private fun mergeIncrementalData(incrementalItem: JsonMap) { - val id = incrementalItem["id"] as String? ?: error("No id found in incremental result") - val data = incrementalItem["data"] as JsonMap? - val items = incrementalItem["items"] as List? - val subPath = incrementalItem["subPath"] as List? ?: emptyList() - 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) - when { - data != null -> { - deepMergeObject(nodeToMergeInto as MutableJsonMap, data) - } - - items != null -> { - mergeList(nodeToMergeInto as MutableList, items) - } - - else -> { - error("Neither data nor items found in incremental result") - } - } - } - - private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { - for ((key, value) in obj) { - if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { - // Objects: merge recursively - val fieldDestination = destination[key] as MutableJsonMap - val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") - deepMergeObject(destination = fieldDestination, obj = fieldMap) - } else { - // Other types: add / overwrite - destination[key] = value - } - } - } - - private fun mergeList(destination: MutableList, items: List) { - destination.addAll(items) - } - - private fun jsonToMap(json: BufferedSource): JsonMap = BufferedSourceJsonReader(json).readAny() as JsonMap - - - /** - * Find the node in the [map] at the given [path]. - * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). - */ - private fun nodeAtPath(map: JsonMap, path: List): Any? { - var node: Any? = map - for (key in path) { - node = if (node is List<*>) { - node[key as Int] - } else { - node as JsonMap - node[key] - } - } - return node - } - - fun reset() { - _merged.clear() - _pendingFragmentIds.clear() - hasNext = true - isEmptyPayload = false - } -} - -internal fun JsonMap.isDeferred(): Boolean { - return keys.contains("hasNext") -} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt new file mode 100644 index 00000000000..b49e1af3dd0 --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt @@ -0,0 +1,186 @@ +package com.apollographql.apollo.internal + +import com.apollographql.apollo.annotations.ApolloInternal +import com.apollographql.apollo.api.IncrementalResultIdentifier +import com.apollographql.apollo.api.json.BufferedSourceJsonReader +import com.apollographql.apollo.api.json.readAny +import okio.BufferedSource + +private typealias JsonMap = Map +private typealias MutableJsonMap = MutableMap + +/** + * Utility class for merging GraphQL incremental results received in multiple chunks when using the `@defer` and/or `@stream` directives. + * + * Each call to [merge] will merge the given results into the [merged] Map, and will also update the [pendingResultIds] Set with the + * value of their `path` and `label` fields. + * + * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field. For the first call to + * [merge], the payload is copied to [merged] as-is. + * + * `errors` in incremental and completed results (if present) are merged together in an array and then set to the `errors` field of the + * [merged] Map. + * `extensions` in incremental results (if present) are merged together in an array and then set to the `extensions` field of the [merged] + * Map. + */ +@ApolloInternal +@Suppress("UNCHECKED_CAST") +class IncrementalResultsMerger { + private val _merged: MutableJsonMap = mutableMapOf() + val merged: JsonMap = _merged + + /** + * Map of identifiers to their corresponding IncrementalResultIdentifier, found in `pending`. + */ + private val _pendingResultIds = mutableMapOf() + val pendingResultIds: Set get() = _pendingResultIds.values.toSet() + + var hasNext: Boolean = true + private set + + /** + * A response can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last + * emitted payload. This field allows to test for this in order to ignore such payloads. + * See https://github.com/apollographql/router/issues/1687. + */ + var isEmptyResponse: Boolean = false + private set + + fun merge(part: BufferedSource): JsonMap { + return merge(part.toJsonMap()) + } + + fun merge(part: JsonMap): JsonMap { + val completed = part["completed"] as? List + if (merged.isEmpty()) { + // Initial part, no merging needed (strip some fields that should not appear in the final result) + _merged += part - "hasNext" - "pending" + handlePending(part) + handleCompleted(completed) + return merged + } + handlePending(part) + + val incremental = part["incremental"] as? List + if (incremental != null) { + for (incrementalResult in incremental) { + mergeIncrementalResult(incrementalResult) + // Merge errors (if any) of the incremental result + (incrementalResult["errors"] as? List)?.let { getOrPutMergedErrors() += it } + } + } + isEmptyResponse = completed == null && incremental == null + + hasNext = part["hasNext"] as Boolean? ?: false + + handleCompleted(completed) + + (part["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } + + return merged + } + + private fun getOrPutMergedErrors() = _merged.getOrPut("errors") { mutableListOf() } as MutableList + + private fun getOrPutExtensions() = _merged.getOrPut("extensions") { mutableMapOf() } as MutableJsonMap + + private fun handlePending(part: JsonMap) { + val pending = part["pending"] as? List + if (pending != null) { + for (pendingResult in pending) { + val id = pendingResult["id"] as String + val path = pendingResult["path"] as List + val label = pendingResult["label"] as String? + _pendingResultIds[id] = IncrementalResultIdentifier(path = path, label = label) + } + } + } + + private fun handleCompleted(completed: List?) { + if (completed != null) { + for (completedResult in completed) { + // Merge errors (if any) of the completed result + val errors = completedResult["errors"] as? List + if (errors != null) { + getOrPutMergedErrors() += errors + } else { + // Fragment is no longer pending - only if there were no errors + val id = completedResult["id"] as String + _pendingResultIds.remove(id) ?: error("Id '$id' not found in pending results") + } + } + } + } + + private fun mergeIncrementalResult(incrementalResult: JsonMap) { + val id = incrementalResult["id"] as String? ?: error("No id found in incremental result") + val data = incrementalResult["data"] as JsonMap? + val items = incrementalResult["items"] as List? + val subPath = incrementalResult["subPath"] as List? ?: emptyList() + val path = (_pendingResultIds[id]?.path ?: error("Id '$id' not found in pending results")) + subPath + val mergedData = merged["data"] as JsonMap + val nodeToMergeInto = nodeAtPath(mergedData, path) + when { + data != null -> { + deepMergeObject(nodeToMergeInto as MutableJsonMap, data) + } + + items != null -> { + mergeList(nodeToMergeInto as MutableList, items) + } + + else -> { + error("Neither data nor items found in incremental result") + } + } + } + + private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { + for ((key, value) in obj) { + if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { + // Objects: merge recursively + val fieldDestination = destination[key] as MutableJsonMap + val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") + deepMergeObject(destination = fieldDestination, obj = fieldMap) + } else { + // Other types: add / overwrite + destination[key] = value + } + } + } + + private fun mergeList(destination: MutableList, items: List) { + destination.addAll(items) + } + + private fun BufferedSource.toJsonMap(): JsonMap = BufferedSourceJsonReader(this).readAny() as JsonMap + + + /** + * Find the node in the [map] at the given [path]. + * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). + */ + private fun nodeAtPath(map: JsonMap, path: List): Any? { + var node: Any? = map + for (key in path) { + node = if (node is List<*>) { + node[key as Int] + } else { + node as JsonMap + node[key] + } + } + return node + } + + fun reset() { + _merged.clear() + _pendingResultIds.clear() + hasNext = true + isEmptyResponse = false + } +} + +internal fun JsonMap.isIncremental(): Boolean { + return keys.contains("hasNext") +} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 2f332ce8a11..959f7a6a16d 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -21,7 +21,7 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloHttpException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.RouterError -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import com.apollographql.apollo.internal.isGraphQLResponse import com.apollographql.apollo.internal.isMultipart import com.apollographql.apollo.internal.multipartBodyFlow @@ -170,7 +170,7 @@ private constructor( customScalarAdapters: CustomScalarAdapters, httpResponse: HttpResponse, ): Flow> { - var jsonMerger: DeferredJsonMerger? = null + var incrementalResultsMerger: IncrementalResultsMerger? = null val operation = request.operation return multipartBodyFlow(httpResponse) @@ -218,21 +218,20 @@ private constructor( else -> null } } else { - if (jsonMerger == null) { - jsonMerger = DeferredJsonMerger() + if (incrementalResultsMerger == null) { + incrementalResultsMerger = IncrementalResultsMerger() } - val merged = jsonMerger.merge(part) - val deferredFragmentIds = jsonMerger.pendingFragmentIds - val isLast = !jsonMerger.hasNext + val merged = incrementalResultsMerger.merge(part) + val pendingResultIds = incrementalResultsMerger.pendingResultIds + val isLast = !incrementalResultsMerger.hasNext - if (jsonMerger.isEmptyPayload) { + if (incrementalResultsMerger.isEmptyResponse) { null } else { - @Suppress("DEPRECATION") merged.jsonReader().toApolloResponse( operation = operation, customScalarAdapters = customScalarAdapters, - deferredFragmentIdentifiers = deferredFragmentIds + deferredFragmentIdentifiers = pendingResultIds ).newBuilder().isLast(isLast).build() } } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt index 1bfe1f0b98a..e6e2370c49f 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt @@ -12,7 +12,7 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloWebSocketForceCloseException import com.apollographql.apollo.exception.DefaultApolloException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.websocket.internal.OperationListener import com.apollographql.apollo.network.websocket.internal.WebSocketPool @@ -202,7 +202,7 @@ private object DefaultSubscriptionParserFactory: SubscriptionParserFactory { } private class DefaultSubscriptionParser(private val request: ApolloRequest) : SubscriptionParser { - private var deferredJsonMerger: DeferredJsonMerger = DeferredJsonMerger() + private var incrementalResultsMerger: IncrementalResultsMerger = IncrementalResultsMerger() private val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty @Suppress("NAME_SHADOWING") @@ -215,7 +215,7 @@ private class DefaultSubscriptionParser(private val request: } val (payload, mergedFragmentIds) = if (responseMap.isDeferred()) { - deferredJsonMerger.merge(responseMap) to deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.merge(responseMap) to incrementalResultsMerger.pendingResultIds } else { responseMap to null } @@ -226,12 +226,12 @@ private class DefaultSubscriptionParser(private val request: deferredFragmentIdentifiers = mergedFragmentIds ) - if (!deferredJsonMerger.hasNext) { - // Last deferred payload: reset the deferredJsonMerger for potential subsequent responses - deferredJsonMerger.reset() + if (!incrementalResultsMerger.hasNext) { + // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() } - return if (deferredJsonMerger.isEmptyPayload) { + return if (incrementalResultsMerger.isEmptyResponse) { null } else { apolloResponse diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt index 8ffda6b9285..549638ed687 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt @@ -10,8 +10,8 @@ import com.apollographql.apollo.api.toApolloResponse import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.DeferredJsonMerger -import com.apollographql.apollo.internal.isDeferred +import com.apollographql.apollo.internal.IncrementalResultsMerger +import com.apollographql.apollo.internal.isIncremental import com.apollographql.apollo.internal.transformWhile import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.ws.internal.Command @@ -262,7 +262,7 @@ private constructor( override fun execute( request: ApolloRequest, ): Flow> { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() return events.onSubscription { messages.send(StartOperation(request)) @@ -302,8 +302,8 @@ private constructor( is OperationResponse -> { val responsePayload = response.payload val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters]!! - val (payload, mergedFragmentIds) = if (responsePayload.isDeferred()) { - deferredJsonMerger.merge(responsePayload) to deferredJsonMerger.pendingFragmentIds + val (payload, mergedFragmentIds) = if (responsePayload.isIncremental()) { + incrementalResultsMerger.merge(responsePayload) to incrementalResultsMerger.pendingResultIds } else { responsePayload to null } @@ -314,9 +314,9 @@ private constructor( deferredFragmentIdentifiers = mergedFragmentIds ) - if (!deferredJsonMerger.hasNext) { - // Last deferred payload: reset the deferredJsonMerger for potential subsequent responses - deferredJsonMerger.reset() + if (!incrementalResultsMerger.hasNext) { + // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() } apolloResponse } @@ -328,7 +328,7 @@ private constructor( is ConnectionReEstablished, is OperationComplete, is GeneralError -> error("Unexpected event $response") } }.filterNot { - deferredJsonMerger.isEmptyPayload + incrementalResultsMerger.isEmptyResponse }.onCompletion { messages.send(StopOperation(request)) } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt similarity index 78% rename from libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt rename to libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt index f1c189ad919..e743fa6f2ac 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt @@ -3,10 +3,10 @@ package test.defer import com.apollographql.apollo.annotations.ApolloInternal -import com.apollographql.apollo.api.DeferredFragmentIdentifier +import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.json.BufferedSourceJsonReader import com.apollographql.apollo.api.json.readAny -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import okio.Buffer import kotlin.test.Test import kotlin.test.assertEquals @@ -18,10 +18,10 @@ private fun String.buffer() = Buffer().writeUtf8(this) @Suppress("UNCHECKED_CAST") private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map -class DeferredJsonMergerTest { +class IncrementalResultsMergerTest { @Test fun mergeJsonSingleIncrementalItem() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -76,13 +76,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0") ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -154,13 +154,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0") + IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0") ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -236,13 +236,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -334,14 +334,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload4.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -442,19 +442,19 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @Test fun mergeJsonMultipleIncrementalItems() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -517,14 +517,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -622,14 +622,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2_3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2_3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -750,19 +750,19 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload4_5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload4_5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @Test fun emptyPayloads() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -804,8 +804,8 @@ class DeferredJsonMergerTest { "hasNext": true } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload1.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) //language=JSON val payload2 = """ @@ -813,8 +813,8 @@ class DeferredJsonMergerTest { "hasNext": true } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload2.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) //language=JSON val payload3 = """ { @@ -833,8 +833,8 @@ class DeferredJsonMergerTest { "hasNext": true } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload3.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) //language=JSON val payload4 = """ @@ -842,8 +842,8 @@ class DeferredJsonMergerTest { "hasNext": false } """.trimIndent() - deferredJsonMerger.merge(payload4.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload4.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) } /** @@ -851,7 +851,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleA() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -897,13 +897,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf(), label = null), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -957,11 +957,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -970,7 +970,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleA2() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1017,13 +1017,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "D1"), + IncrementalResultIdentifier(path = listOf(), label = "D1"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1083,13 +1083,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), + IncrementalResultIdentifier(path = listOf("f2", "c", "f"), label = "D2"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1136,11 +1136,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1149,7 +1149,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleB1() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1195,14 +1195,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1250,13 +1250,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf(), label = "Blue"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1303,11 +1303,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1316,7 +1316,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleB2() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1362,14 +1362,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1423,13 +1423,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1472,11 +1472,11 @@ class DeferredJsonMergerTest { } } """ - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1485,7 +1485,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleD() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1515,14 +1515,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), - DeferredFragmentIdentifier(path = listOf("me"), label = null), + IncrementalResultIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf("me"), label = null), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1613,13 +1613,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf(), label = null), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1700,11 +1700,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1713,7 +1713,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleF() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1740,13 +1740,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1780,11 +1780,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1793,7 +1793,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleG() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1843,14 +1843,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "Billing"), - DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + IncrementalResultIdentifier(path = listOf("me"), label = "Billing"), + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1893,13 +1893,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1949,11 +1949,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1962,7 +1962,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleH() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1994,14 +1994,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "A"), - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf(), label = "A"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2053,13 +2053,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2119,13 +2119,13 @@ class DeferredJsonMergerTest { ] } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -2134,7 +2134,7 @@ class DeferredJsonMergerTest { */ @Test fun july2025ExampleI() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -2189,14 +2189,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2236,14 +2236,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2279,13 +2279,13 @@ class DeferredJsonMergerTest { } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2334,11 +2334,11 @@ class DeferredJsonMergerTest { } """.trimIndent() - deferredJsonMerger.merge(payload4.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -2347,7 +2347,7 @@ class DeferredJsonMergerTest { */ @Test fun july2025ExampleJ() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -2387,13 +2387,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2429,13 +2429,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2497,13 +2497,13 @@ class DeferredJsonMergerTest { } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } } From 5e4058b24927a95bc6eae40ce4b041cd5b8ee5d1 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 21 Jul 2025 10:00:31 +0200 Subject: [PATCH 9/9] Fix running tests hardcoded to true --- tests/defer/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index 0f1b0ea79c7..d90c4f5fd6d 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -77,7 +77,7 @@ fun com.apollographql.apollo.gradle.api.Service.configureConnection(generateKotl tasks.withType(AbstractTestTask::class.java) { // 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() - val runDeferWithApolloServerTests = true + val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() filter.setIncludePatterns(*buildList { if (runDeferWithRouterTests) add("test.DeferWithRouterTest") if (runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest")