From 8d486fbad88a03f9d950e53843ea0146e7560954 Mon Sep 17 00:00:00 2001 From: Franklin Wang <9077461+gnawf@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:09:06 +1100 Subject: [PATCH] Add ability to migrate hydration based on type conditions (#667) --- .../hydration/NadelHydrationTransform.kt | 10 +- .../hydration/batch/NadelNewBatchHydrator.kt | 2 + .../nadel/hooks/NadelExecutionHooks.kt | 2 + ...hydration-instruction-hook-returns-null.kt | 5 +- .../hooks/batching-absent-source-input.kt | 5 +- ...-conditional-hydration-in-abstract-type.kt | 4 +- ...-source-ids-going-to-different-services.kt | 4 +- ...tching-of-hydration-list-with-partition.kt | 4 +- .../tests/hooks/batching-single-source-id.kt | 2 + .../hooks/new-batching-no-source-inputs.kt | 4 +- ...rphic-hydration-hook-using-alias-helper.kt | 2 + .../hooks/polymorphic-hydration-hooks.kt | 2 + ...ation-instructions-use-different-inputs.kt | 3 + ...icHydrationCommonInterfaceMigrationTest.kt | 134 ++++++ ...ionCommonInterfaceMigrationTestSnapshot.kt | 113 +++++ .../PolymorphicHydrationMigrationTest.kt | 159 +++++++ ...lymorphicHydrationMigrationTestSnapshot.kt | 430 ++++++++++++++++++ ...ticHydrationAndPolymorphicHydrationTest.kt | 2 + .../schema/CustomHydrationDirectiveTest.kt | 2 + 19 files changed, 879 insertions(+), 10 deletions(-) create mode 100644 test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTest.kt create mode 100644 test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTestSnapshot.kt create mode 100644 test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTest.kt create mode 100644 test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTestSnapshot.kt diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationTransform.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationTransform.kt index 54449f441..e45dc9995 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationTransform.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationTransform.kt @@ -417,12 +417,14 @@ internal class NadelHydrationTransform( ): NadelHydrationFieldInstruction? { if (instructions.any { it.condition == null }) { return hooks.getHydrationInstruction( - instructions, - parentNode, - state.aliasHelper, - state.executionContext.userContext + virtualField = state.virtualField, + instructions = instructions, + parentNode = parentNode, + aliasHelper = state.aliasHelper, + userContext = state.executionContext.userContext ) } + return instructions .firstOrNull { // Note: due to the validation, all instructions in here have a condition, so can call explicitly diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt index 0fb68c302..479e01e90 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt @@ -585,6 +585,7 @@ internal class NadelNewBatchHydrator( ): NadelBatchHydrationFieldInstruction? { if (instructions.any { it.condition == null }) { return executionContext.hooks.getHydrationInstruction( + virtualField = sourceField, instructions = instructions, sourceInput = sourceInput, userContext = executionContext.userContext, @@ -612,6 +613,7 @@ internal class NadelNewBatchHydrator( ): NadelBatchHydrationFieldInstruction? { if (instructions.any { it.condition == null }) { return executionContext.hooks.getHydrationInstruction( + virtualField = sourceField, instructions = instructions, parentNode = sourceObject, aliasHelper = aliasHelper, diff --git a/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt b/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt index b9f8f486f..3658d994c 100644 --- a/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt +++ b/lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt @@ -45,6 +45,7 @@ interface NadelExecutionHooks { } fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, @@ -54,6 +55,7 @@ interface NadelExecutionHooks { } fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, sourceInput: JsonNode, userContext: Any?, diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/batch-hydration-instruction-hook-returns-null.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/batch-hydration-instruction-hook-returns-null.kt index 6d25d772b..c045a0338 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/batch-hydration-instruction-hook-returns-null.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/batch-hydration-instruction-hook-returns-null.kt @@ -7,6 +7,7 @@ import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField @UseHook class `batch-hydration-instruction-hook-returns-null` : EngineTestHook { @@ -15,15 +16,17 @@ class `batch-hydration-instruction-hook-returns-null` : EngineTestHook { .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, userContext: Any?, - ): Nothing { + ): T? { throw UnsupportedOperationException("should not run") } override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, sourceInput: JsonNode, userContext: Any?, diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-absent-source-input.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-absent-source-input.kt index 3a06ff517..f66344829 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-absent-source-input.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-absent-source-input.kt @@ -7,6 +7,7 @@ import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField @UseHook class `batching-absent-source-input` : EngineTestHook { @@ -15,10 +16,11 @@ class `batching-absent-source-input` : EngineTestHook { .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, sourceId: JsonNode, userContext: Any?, - ): T { + ): T? { val type = (sourceId.value as String).substringBefore("/") return instructions @@ -28,6 +30,7 @@ class `batching-absent-source-input` : EngineTestHook { } override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-conditional-hydration-in-abstract-type.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-conditional-hydration-in-abstract-type.kt index afc979e72..bf0d45ba1 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-conditional-hydration-in-abstract-type.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-conditional-hydration-in-abstract-type.kt @@ -6,6 +6,7 @@ import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField @UseHook class `batching-conditional-hydration-in-abstract-type` : EngineTestHook { @@ -14,10 +15,11 @@ class `batching-conditional-hydration-in-abstract-type` : EngineTestHook { .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, sourceInput: JsonNode, userContext: Any?, - ): T { + ): T? { val type = (sourceInput.value as String).substringBefore("/") return instructions diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-multiple-source-ids-going-to-different-services.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-multiple-source-ids-going-to-different-services.kt index 77d9ef949..f92f3b3f6 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-multiple-source-ids-going-to-different-services.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-multiple-source-ids-going-to-different-services.kt @@ -6,6 +6,7 @@ import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField @UseHook class `batching-multiple-source-ids-going-to-different-services` : EngineTestHook { @@ -14,10 +15,11 @@ class `batching-multiple-source-ids-going-to-different-services` : EngineTestHoo .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, sourceInput: JsonNode, userContext: Any?, - ): T { + ): T? { val type = (sourceInput.value as String).substringBefore("/") return instructions diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-of-hydration-list-with-partition.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-of-hydration-list-with-partition.kt index 052f80c63..1c3d8100a 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-of-hydration-list-with-partition.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-of-hydration-list-with-partition.kt @@ -8,14 +8,16 @@ import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField private class BatchHydrationHooks : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, userContext: Any?, - ): T { + ): T? { return instructions[0] } diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-single-source-id.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-single-source-id.kt index db4595894..74038a4be 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-single-source-id.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/batching-single-source-id.kt @@ -10,6 +10,7 @@ import graphql.nadel.engine.util.singleOfType import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField @UseHook class `batching-single-source-id` : EngineTestHook { @@ -18,6 +19,7 @@ class `batching-single-source-id` : EngineTestHook { .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/new-batching-no-source-inputs.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/new-batching-no-source-inputs.kt index 8234ddf3d..fba396441 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/new-batching-no-source-inputs.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/new-batching-no-source-inputs.kt @@ -6,6 +6,7 @@ import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField @UseHook class `batching-no-source-inputs` : EngineTestHook { @@ -14,10 +15,11 @@ class `batching-no-source-inputs` : EngineTestHook { .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, sourceId: JsonNode, userContext: Any?, - ): T { + ): T? { val type = (sourceId.value as String).substringBefore("/") return instructions diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hook-using-alias-helper.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hook-using-alias-helper.kt index 11a68579f..1d64a5230 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hook-using-alias-helper.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hook-using-alias-helper.kt @@ -14,6 +14,7 @@ import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook import graphql.nadel.tests.util.serviceExecutionFactory +import graphql.normalized.ExecutableNormalizedField import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -25,6 +26,7 @@ import kotlin.time.Duration.Companion.milliseconds private class PolymorphicHydrationHookUsingAliasHelper : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hooks.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hooks.kt index 98e8e2b4c..263376597 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hooks.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-hooks.kt @@ -9,9 +9,11 @@ import graphql.nadel.engine.util.JsonMap import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField private class PolymorphicHydrationHooks : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-instructions-use-different-inputs.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-instructions-use-different-inputs.kt index 9424b3bd8..e07e555f6 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-instructions-use-different-inputs.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/polymorphic-hydration-instructions-use-different-inputs.kt @@ -7,6 +7,7 @@ import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.EngineTestHook import graphql.nadel.tests.UseHook +import graphql.normalized.ExecutableNormalizedField @UseHook class `polymorphic-hydration-instructions-use-different-inputs` : EngineTestHook { @@ -15,6 +16,7 @@ class `polymorphic-hydration-instructions-use-different-inputs` : EngineTestHook .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, @@ -31,6 +33,7 @@ class `polymorphic-hydration-instructions-use-different-inputs` : EngineTestHook } override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, sourceInput: JsonNode, userContext: Any?, diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTest.kt new file mode 100644 index 000000000..32f03e17c --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTest.kt @@ -0,0 +1,134 @@ +package graphql.nadel.tests.next.fixtures.hydration + +import graphql.nadel.Nadel +import graphql.nadel.engine.blueprint.NadelGenericHydrationInstruction +import graphql.nadel.engine.transform.artificial.NadelAliasHelper +import graphql.nadel.engine.transform.result.json.JsonNode +import graphql.nadel.hooks.NadelExecutionHooks +import graphql.nadel.tests.next.NadelIntegrationTest +import graphql.normalized.ExecutableNormalizedField + +/** + * Should resolve to old hydration if it's ambiguous. + */ +class PolymorphicHydrationCommonInterfaceMigrationTest : NadelIntegrationTest( + query = """ + { + reference { + object { + __typename + ... on OldObject { + name + } + ... on CommonInterface { + id + } + ... on NewObject { + id + } + } + } + } + """.trimIndent(), + services = listOf( + Service( + name = "monolith", + overallSchema = """ + type Query { + reference: Reference + oldObjects(ids: [ID!]!): [OldObject] + newObjects(ids: [ID!]!): [NewObject] + } + union Object = OldObject | NewObject + type Reference { + objectId: ID! + object: Object + @idHydrated(idField: "objectId") + } + interface CommonInterface { + id: ID! + } + type NewObject implements CommonInterface @defaultHydration(field: "newObjects", idArgument: "ids") { + id: ID! + name: String + } + type OldObject implements CommonInterface @defaultHydration(field: "oldObjects", idArgument: "ids") { + id: ID! + name: String + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class Reference(val objectId: String) + data class NewObject(val id: String, val name: String) + data class OldObject(val id: String, val name: String) + + val newObjectsById = listOf( + NewObject("ari:cloud:owner::type/1", "New object") + ).associateBy { it.id } + + val oldObjectsById = listOf( + OldObject("ari:cloud:owner::type/1", "Old object") + ).associateBy { it.id } + + wiring + .type("Query") { type -> + type + .dataFetcher("reference") { env -> + Reference(objectId = "ari:cloud:owner::type/1") + } + .dataFetcher("oldObjects") { env -> + env.getArgument>("ids")?.map(oldObjectsById::get) + } + .dataFetcher("newObjects") { env -> + env.getArgument>("ids")?.map(newObjectsById::get) + } + } + .type("Object") { type -> + type.typeResolver { env -> + env.schema.getObjectType(env.getObject().javaClass.simpleName) + } + } + .type("CommonInterface") { type -> + type.typeResolver { env -> + env.schema.getObjectType(env.getObject().javaClass.simpleName) + } + } + }, + ), + ), +) { + override fun makeNadel(): Nadel.Builder { + return super.makeNadel() + .executionHooks( + object : NadelExecutionHooks { + override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, + instructions: List, + parentNode: JsonNode, + aliasHelper: NadelAliasHelper, + userContext: Any?, + ): T? { + val hasNewObject = virtualField.children.any { child -> + child.objectTypeNames.size == 1 + && child.objectTypeNames.first() == "NewObject" + && !child.fieldName.startsWith("__") + } + val hasOldObject = virtualField.children.any { child -> + child.objectTypeNames.contains("OldObject") + && !child.fieldName.startsWith("__") + } + + return if (hasNewObject || !hasOldObject) { + instructions.first { + it.backingFieldDef.name == "newObjects" + } + } else { + instructions.first { + it.backingFieldDef.name == "oldObjects" + } + } + } + } + ) + } +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTestSnapshot.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTestSnapshot.kt new file mode 100644 index 000000000..27a41b436 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationCommonInterfaceMigrationTestSnapshot.kt @@ -0,0 +1,113 @@ +// @formatter:off +package graphql.nadel.tests.next.fixtures.hydration + +import graphql.nadel.tests.next.ExpectedNadelResult +import graphql.nadel.tests.next.ExpectedServiceCall +import graphql.nadel.tests.next.TestSnapshot +import graphql.nadel.tests.next.listOfJsonStrings +import kotlin.Suppress +import kotlin.collections.List +import kotlin.collections.listOf + +private suspend fun main() { + graphql.nadel.tests.next.update() +} + +/** + * This class is generated. Do NOT modify. + * + * Refer to [graphql.nadel.tests.next.UpdateTestSnapshots] + */ +@Suppress("unused") +public class PolymorphicHydrationCommonInterfaceMigrationTestSnapshot : TestSnapshot() { + override val calls: List = listOf( + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | oldObjects(ids: ["ari:cloud:owner::type/1"]) { + | __typename + | id + | batch_hydration__object__id: id + | name + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "oldObjects": [ + | { + | "__typename": "OldObject", + | "name": "Old object", + | "id": "ari:cloud:owner::type/1", + | "batch_hydration__object__id": "ari:cloud:owner::type/1" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | reference { + | __typename__batch_hydration__object: __typename + | batch_hydration__object__objectId: objectId + | batch_hydration__object__objectId: objectId + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "reference": { + | "batch_hydration__object__objectId": "ari:cloud:owner::type/1", + | "__typename__batch_hydration__object": "Reference" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ) + + /** + * ```json + * { + * "data": { + * "reference": { + * "object": { + * "__typename": "OldObject", + * "name": "Old object", + * "id": "ari:cloud:owner::type/1" + * } + * } + * } + * } + * ``` + */ + override val result: ExpectedNadelResult = ExpectedNadelResult( + result = """ + | { + | "data": { + | "reference": { + | "object": { + | "__typename": "OldObject", + | "name": "Old object", + | "id": "ari:cloud:owner::type/1" + | } + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ) +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTest.kt new file mode 100644 index 000000000..16814192e --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTest.kt @@ -0,0 +1,159 @@ +package graphql.nadel.tests.next.fixtures.hydration + +import graphql.nadel.Nadel +import graphql.nadel.engine.blueprint.NadelGenericHydrationInstruction +import graphql.nadel.engine.transform.artificial.NadelAliasHelper +import graphql.nadel.engine.transform.result.json.JsonNode +import graphql.nadel.hooks.NadelExecutionHooks +import graphql.nadel.tests.next.NadelIntegrationTest +import graphql.normalized.ExecutableNormalizedField + +class PolymorphicHydrationMigrationTest : NadelIntegrationTest( + query = """ + { + nothing: reference { + object { + __typename + } + } + old_query: reference { + object { + __typename + ... on OldObject { + id + } + } + } + new_query: reference { + object { + __typename + ... on NewObject { + id + } + } + } + both_query: reference { + object { + __typename + ... on OldObject { + id + } + ... on NewObject { + id + } + } + } + migrate_off: reference { + object { + __typename + ... on OldObject { + id + } + ... on NewObject @include(if: false) { + id + } + } + } + migrate_on: reference { + object { + __typename + ... on OldObject { + id + } + ... on NewObject @include(if: true) { + id + } + } + } + } + """.trimIndent(), + services = listOf( + Service( + name = "monolith", + overallSchema = """ + type Query { + reference: Reference + oldObjects(ids: [ID!]!): [OldObject] + newObjects(ids: [ID!]!): [NewObject] + } + union Object = OldObject | NewObject + type Reference { + objectId: ID! + object: Object + @idHydrated(idField: "objectId") + } + type NewObject @defaultHydration(field: "newObjects", idArgument: "ids") { + id: ID! + } + type OldObject @defaultHydration(field: "oldObjects", idArgument: "ids") { + id: ID! + } + """.trimIndent(), + runtimeWiring = { wiring -> + data class Reference(val objectId: String) + data class NewObject(val id: String) + data class OldObject(val id: String) + + val newObjectsById = listOf( + NewObject("ari:cloud:owner::type/1") + ).associateBy { it.id } + val oldObjectsById = listOf( + OldObject("ari:cloud:owner::type/1") + ).associateBy { it.id } + + wiring + .type("Query") { type -> + type + .dataFetcher("reference") { env -> + Reference(objectId = "ari:cloud:owner::type/1") + } + .dataFetcher("oldObjects") { env -> + env.getArgument>("ids")?.map(oldObjectsById::get) + } + .dataFetcher("newObjects") { env -> + env.getArgument>("ids")?.map(newObjectsById::get) + } + } + .type("Object") { type -> + type.typeResolver { env -> + env.schema.getObjectType(env.getObject().javaClass.simpleName) + } + } + }, + ), + ), +) { + override fun makeNadel(): Nadel.Builder { + return super.makeNadel() + .executionHooks( + object : NadelExecutionHooks { + override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, + instructions: List, + parentNode: JsonNode, + aliasHelper: NadelAliasHelper, + userContext: Any?, + ): T? { + val hasNewObject = virtualField.children.any { child -> + child.objectTypeNames.size == 1 + && child.objectTypeNames.first() == "NewObject" + && !child.fieldName.startsWith("__") + } + val hasOldObject = virtualField.children.any { child -> + child.objectTypeNames.contains("OldObject") && !child.fieldName.startsWith("__") + } + + return if (hasNewObject || !hasOldObject) { + instructions.first { + it.backingFieldDef.name == "newObjects" + } + } else { + instructions.first { + it.backingFieldDef.name == "oldObjects" + } + } + } + } + ) + } +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTestSnapshot.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTestSnapshot.kt new file mode 100644 index 000000000..5eea67a85 --- /dev/null +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/PolymorphicHydrationMigrationTestSnapshot.kt @@ -0,0 +1,430 @@ +// @formatter:off +package graphql.nadel.tests.next.fixtures.hydration + +import graphql.nadel.tests.next.ExpectedNadelResult +import graphql.nadel.tests.next.ExpectedServiceCall +import graphql.nadel.tests.next.TestSnapshot +import graphql.nadel.tests.next.listOfJsonStrings +import kotlin.Suppress +import kotlin.collections.List +import kotlin.collections.listOf + +private suspend fun main() { + graphql.nadel.tests.next.update() +} + +/** + * This class is generated. Do NOT modify. + * + * Refer to [graphql.nadel.tests.next.UpdateTestSnapshots] + */ +@Suppress("unused") +public class PolymorphicHydrationMigrationTestSnapshot : TestSnapshot() { + override val calls: List = listOf( + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | both_query: reference { + | __typename__batch_hydration__object: __typename + | batch_hydration__object__objectId: objectId + | batch_hydration__object__objectId: objectId + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "both_query": { + | "batch_hydration__object__objectId": "ari:cloud:owner::type/1", + | "__typename__batch_hydration__object": "Reference" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | migrate_off: reference { + | __typename__batch_hydration__object: __typename + | batch_hydration__object__objectId: objectId + | batch_hydration__object__objectId: objectId + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "migrate_off": { + | "batch_hydration__object__objectId": "ari:cloud:owner::type/1", + | "__typename__batch_hydration__object": "Reference" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | migrate_on: reference { + | __typename__batch_hydration__object: __typename + | batch_hydration__object__objectId: objectId + | batch_hydration__object__objectId: objectId + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "migrate_on": { + | "batch_hydration__object__objectId": "ari:cloud:owner::type/1", + | "__typename__batch_hydration__object": "Reference" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | newObjects(ids: ["ari:cloud:owner::type/1"]) { + | __typename + | batch_hydration__object__id: id + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "newObjects": [ + | { + | "__typename": "NewObject", + | "batch_hydration__object__id": "ari:cloud:owner::type/1" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | newObjects(ids: ["ari:cloud:owner::type/1"]) { + | __typename + | id + | batch_hydration__object__id: id + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "newObjects": [ + | { + | "__typename": "NewObject", + | "id": "ari:cloud:owner::type/1", + | "batch_hydration__object__id": "ari:cloud:owner::type/1" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | newObjects(ids: ["ari:cloud:owner::type/1"]) { + | __typename + | id + | batch_hydration__object__id: id + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "newObjects": [ + | { + | "__typename": "NewObject", + | "id": "ari:cloud:owner::type/1", + | "batch_hydration__object__id": "ari:cloud:owner::type/1" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | newObjects(ids: ["ari:cloud:owner::type/1"]) { + | __typename + | id + | batch_hydration__object__id: id + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "newObjects": [ + | { + | "__typename": "NewObject", + | "id": "ari:cloud:owner::type/1", + | "batch_hydration__object__id": "ari:cloud:owner::type/1" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | new_query: reference { + | __typename__batch_hydration__object: __typename + | batch_hydration__object__objectId: objectId + | batch_hydration__object__objectId: objectId + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "new_query": { + | "batch_hydration__object__objectId": "ari:cloud:owner::type/1", + | "__typename__batch_hydration__object": "Reference" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | nothing: reference { + | __typename__batch_hydration__object: __typename + | batch_hydration__object__objectId: objectId + | batch_hydration__object__objectId: objectId + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "nothing": { + | "batch_hydration__object__objectId": "ari:cloud:owner::type/1", + | "__typename__batch_hydration__object": "Reference" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | oldObjects(ids: ["ari:cloud:owner::type/1"]) { + | __typename + | id + | batch_hydration__object__id: id + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "oldObjects": [ + | { + | "__typename": "OldObject", + | "id": "ari:cloud:owner::type/1", + | "batch_hydration__object__id": "ari:cloud:owner::type/1" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | oldObjects(ids: ["ari:cloud:owner::type/1"]) { + | __typename + | id + | batch_hydration__object__id: id + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "oldObjects": [ + | { + | "__typename": "OldObject", + | "id": "ari:cloud:owner::type/1", + | "batch_hydration__object__id": "ari:cloud:owner::type/1" + | } + | ] + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ExpectedServiceCall( + service = "monolith", + query = """ + | { + | old_query: reference { + | __typename__batch_hydration__object: __typename + | batch_hydration__object__objectId: objectId + | batch_hydration__object__objectId: objectId + | } + | } + """.trimMargin(), + variables = "{}", + result = """ + | { + | "data": { + | "old_query": { + | "batch_hydration__object__objectId": "ari:cloud:owner::type/1", + | "__typename__batch_hydration__object": "Reference" + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ), + ) + + /** + * ```json + * { + * "data": { + * "nothing": { + * "object": { + * "__typename": "NewObject" + * } + * }, + * "old_query": { + * "object": { + * "__typename": "OldObject", + * "id": "ari:cloud:owner::type/1" + * } + * }, + * "new_query": { + * "object": { + * "__typename": "NewObject", + * "id": "ari:cloud:owner::type/1" + * } + * }, + * "both_query": { + * "object": { + * "__typename": "NewObject", + * "id": "ari:cloud:owner::type/1" + * } + * }, + * "migrate_off": { + * "object": { + * "__typename": "OldObject", + * "id": "ari:cloud:owner::type/1" + * } + * }, + * "migrate_on": { + * "object": { + * "__typename": "NewObject", + * "id": "ari:cloud:owner::type/1" + * } + * } + * } + * } + * ``` + */ + override val result: ExpectedNadelResult = ExpectedNadelResult( + result = """ + | { + | "data": { + | "nothing": { + | "object": { + | "__typename": "NewObject" + | } + | }, + | "old_query": { + | "object": { + | "__typename": "OldObject", + | "id": "ari:cloud:owner::type/1" + | } + | }, + | "new_query": { + | "object": { + | "__typename": "NewObject", + | "id": "ari:cloud:owner::type/1" + | } + | }, + | "both_query": { + | "object": { + | "__typename": "NewObject", + | "id": "ari:cloud:owner::type/1" + | } + | }, + | "migrate_off": { + | "object": { + | "__typename": "OldObject", + | "id": "ari:cloud:owner::type/1" + | } + | }, + | "migrate_on": { + | "object": { + | "__typename": "NewObject", + | "id": "ari:cloud:owner::type/1" + | } + | } + | } + | } + """.trimMargin(), + delayedResults = listOfJsonStrings( + ), + ) +} diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/statics/StaticHydrationAndPolymorphicHydrationTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/statics/StaticHydrationAndPolymorphicHydrationTest.kt index 339682c1d..750cf1c42 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/statics/StaticHydrationAndPolymorphicHydrationTest.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/hydration/statics/StaticHydrationAndPolymorphicHydrationTest.kt @@ -9,6 +9,7 @@ import graphql.nadel.engine.util.JsonMap import graphql.nadel.engine.util.strictAssociateBy import graphql.nadel.hooks.NadelExecutionHooks import graphql.nadel.tests.next.NadelIntegrationTest +import graphql.normalized.ExecutableNormalizedField /** * Uses hydration to "copy" a field. Does not link two pieces of data together i.e. no $source fields used. @@ -270,6 +271,7 @@ class StaticHydrationAndPolymorphicHydrationTest : NadelIntegrationTest( .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper, diff --git a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/schema/CustomHydrationDirectiveTest.kt b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/schema/CustomHydrationDirectiveTest.kt index cfb30f740..7f8fe5a2d 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/schema/CustomHydrationDirectiveTest.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/next/fixtures/schema/CustomHydrationDirectiveTest.kt @@ -19,6 +19,7 @@ import graphql.nadel.tests.next.NadelIntegrationTest import graphql.nadel.validation.NadelSchemaValidation import graphql.nadel.validation.NadelSchemaValidationFactory import graphql.nadel.validation.NadelSchemaValidationHook +import graphql.normalized.ExecutableNormalizedField import graphql.scalars.ExtendedScalars import graphql.schema.GraphQLAppliedDirective import graphql.schema.GraphQLFieldDefinition @@ -267,6 +268,7 @@ class CustomHydrationDirectiveTest : NadelIntegrationTest( .executionHooks( object : NadelExecutionHooks { override fun getHydrationInstruction( + virtualField: ExecutableNormalizedField, instructions: List, parentNode: JsonNode, aliasHelper: NadelAliasHelper,