Skip to content

Commit

Permalink
Add ability to migrate hydration based on type conditions (#667)
Browse files Browse the repository at this point in the history
  • Loading branch information
gnawf authored Jan 13, 2025
1 parent a215479 commit 8d486fb
Show file tree
Hide file tree
Showing 19 changed files with 879 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions lib/src/main/java/graphql/nadel/hooks/NadelExecutionHooks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface NadelExecutionHooks {
}

fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
Expand All @@ -54,6 +55,7 @@ interface NadelExecutionHooks {
}

fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
sourceInput: JsonNode,
userContext: Any?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,15 +16,17 @@ class `batch-hydration-instruction-hook-returns-null` : EngineTestHook {
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
userContext: Any?,
): Nothing {
): T? {
throw UnsupportedOperationException("should not run")
}

override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
sourceInput: JsonNode,
userContext: Any?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,10 +16,11 @@ class `batching-absent-source-input` : EngineTestHook {
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
sourceId: JsonNode,
userContext: Any?,
): T {
): T? {
val type = (sourceId.value as String).substringBefore("/")

return instructions
Expand All @@ -28,6 +30,7 @@ class `batching-absent-source-input` : EngineTestHook {
}

override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -14,10 +15,11 @@ class `batching-conditional-hydration-in-abstract-type` : EngineTestHook {
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
sourceInput: JsonNode,
userContext: Any?,
): T {
): T? {
val type = (sourceInput.value as String).substringBefore("/")

return instructions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -14,10 +15,11 @@ class `batching-multiple-source-ids-going-to-different-services` : EngineTestHoo
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
sourceInput: JsonNode,
userContext: Any?,
): T {
): T? {
val type = (sourceInput.value as String).substringBefore("/")

return instructions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
userContext: Any?,
): T {
): T? {
return instructions[0]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -18,6 +19,7 @@ class `batching-single-source-id` : EngineTestHook {
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -14,10 +15,11 @@ class `batching-no-source-inputs` : EngineTestHook {
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
sourceId: JsonNode,
userContext: Any?,
): T {
): T? {
val type = (sourceId.value as String).substringBefore("/")

return instructions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,6 +26,7 @@ import kotlin.time.Duration.Companion.milliseconds

private class PolymorphicHydrationHookUsingAliasHelper : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,6 +16,7 @@ class `polymorphic-hydration-instructions-use-different-inputs` : EngineTestHook
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
parentNode: JsonNode,
aliasHelper: NadelAliasHelper,
Expand All @@ -31,6 +33,7 @@ class `polymorphic-hydration-instructions-use-different-inputs` : EngineTestHook
}

override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
sourceInput: JsonNode,
userContext: Any?,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<String>>("ids")?.map(oldObjectsById::get)
}
.dataFetcher("newObjects") { env ->
env.getArgument<List<String>>("ids")?.map(newObjectsById::get)
}
}
.type("Object") { type ->
type.typeResolver { env ->
env.schema.getObjectType(env.getObject<Any?>().javaClass.simpleName)
}
}
.type("CommonInterface") { type ->
type.typeResolver { env ->
env.schema.getObjectType(env.getObject<Any?>().javaClass.simpleName)
}
}
},
),
),
) {
override fun makeNadel(): Nadel.Builder {
return super.makeNadel()
.executionHooks(
object : NadelExecutionHooks {
override fun <T : NadelGenericHydrationInstruction> getHydrationInstruction(
virtualField: ExecutableNormalizedField,
instructions: List<T>,
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"
}
}
}
}
)
}
}
Loading

0 comments on commit 8d486fb

Please sign in to comment.