WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 50924f3

Browse files
skevyviaductbot
authored andcommitted
Implement ParentManagedValue for resolver takeover (AIRBNB)
Github-Change-Id: 967108 GitOrigin-RevId: ca1230f4bddc03f25356b930ffd9dcd8d21bb006
1 parent 8efb390 commit 50924f3

File tree

12 files changed

+319
-21
lines changed

12 files changed

+319
-21
lines changed

engine/api/src/main/kotlin/viaduct/engine/api/EngineExecutionContext.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ interface EngineExecutionContext {
8585
* - Child plan execution: The resolved child plan variables
8686
*/
8787
val variables: Map<String, Any?>
88+
89+
/**
90+
* The policy governing how fields within this scope should be resolved.
91+
*
92+
* This is determined by the result of the parent field's execution.
93+
* - [ResolutionPolicy.STANDARD]: Normal execution (lookup resolvers).
94+
* - [ResolutionPolicy.PARENT_MANAGED]: Driven by [ParentManagedValue], skipping resolvers.
95+
*/
96+
val resolutionPolicy: ResolutionPolicy
8897
}
8998

9099
/**
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package viaduct.engine.api
2+
3+
/**
4+
* A wrapper that can be returned by a resolver to signal that it should
5+
* take over the resolution of its nested selections.
6+
*
7+
* Returning this wrapper enables "Parent-Managed Resolution", where the parent
8+
* resolver provides the full data structure for its children, bypassing individual
9+
* field resolvers.
10+
*
11+
* ## Use Cases
12+
* - **Adapters**: When wrapping legacy objects or map structures where you want to
13+
* expose the raw data directly without writing trivial resolvers for every field.
14+
* - **Optimization**: When the parent can fetch the entire subtree more efficiently
15+
* than resolving each field individually.
16+
*
17+
* ## Behavior
18+
* When the engine encounters this wrapper:
19+
* 1. It unwraps the [value] and uses it as the source for the current level.
20+
* 2. It switches the [ResolutionPolicy] to [ResolutionPolicy.PARENT_MANAGED] for all
21+
* nested selections.
22+
* 3. Registered resolvers for child fields are **skipped**.
23+
* 4. Fields are resolved using the default property data fetcher (reading properties/keys
24+
* from the [value]).
25+
*/
26+
@JvmInline
27+
value class ParentManagedValue(val value: Any?) {
28+
init {
29+
require(value !is ParentManagedValue)
30+
}
31+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package viaduct.engine.api
2+
3+
/**
4+
* Defines the policy for resolving nested fields of an object.
5+
*
6+
* This policy controls whether the engine looks up and executes registered resolvers
7+
* for fields, or whether it relies on simple property access on the parent object.
8+
*/
9+
enum class ResolutionPolicy {
10+
/**
11+
* Standard resolution: The engine looks for and executes registered field resolvers.
12+
* If no resolver is found, it falls back to the default property data fetcher.
13+
*/
14+
STANDARD,
15+
16+
/**
17+
* Parent-managed resolution: The parent object has explicitly taken responsibility
18+
* for its child fields (typically by returning a [ParentManagedValue]).
19+
*
20+
* In this mode:
21+
* - Registered resolvers are **ignored**.
22+
* - Fields are resolved purely via the default data fetcher wired for fields, typically
23+
* a `PropertyDataFetcher`.
24+
*/
25+
PARENT_MANAGED
26+
}

engine/runtime/src/main/kotlin/viaduct/engine/runtime/EngineExecutionContextImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import viaduct.engine.api.FragmentLoader
1212
import viaduct.engine.api.NodeResolverExecutor
1313
import viaduct.engine.api.RawSelectionSet
1414
import viaduct.engine.api.RawSelectionsLoader
15+
import viaduct.engine.api.ResolutionPolicy
1516
import viaduct.engine.api.ViaductSchema
1617
import viaduct.engine.runtime.select.RawSelectionSetFactoryImpl
1718
import viaduct.service.api.spi.FlagManager
@@ -87,6 +88,7 @@ class EngineExecutionContextImpl(
8788
data class FieldExecutionScopeImpl(
8889
override val fragments: Map<String, FragmentDefinition> = emptyMap(),
8990
override val variables: Map<String, Any?> = emptyMap(),
91+
override val resolutionPolicy: ResolutionPolicy = ResolutionPolicy.STANDARD,
9092
) : EngineExecutionContext.FieldExecutionScope
9193

9294
override fun createNodeReference(

engine/runtime/src/main/kotlin/viaduct/engine/runtime/FieldResolutionResult.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package viaduct.engine.runtime
22

33
import graphql.GraphQLError
44
import graphql.execution.FetchedValue
5+
import viaduct.engine.api.ResolutionPolicy
56
import viaduct.engine.runtime.context.CompositeLocalContext
67

78
/**
@@ -16,6 +17,7 @@ data class FieldResolutionResult(
1617
val localContext: CompositeLocalContext,
1718
val extensions: Map<Any, Any?>,
1819
val originalSource: Any?,
20+
val resolutionPolicy: ResolutionPolicy = ResolutionPolicy.STANDARD,
1921
) {
2022
companion object {
2123
private val Any?.asCompositeLocalContext: CompositeLocalContext
@@ -26,9 +28,20 @@ data class FieldResolutionResult(
2628
throw IllegalStateException("Expected CompositeLocalContext but found ${ctx::class}")
2729
}
2830

31+
fun fromErrors(errors: List<GraphQLError>,) =
32+
FieldResolutionResult(
33+
engineResult = null,
34+
errors = errors,
35+
localContext = CompositeLocalContext.empty,
36+
extensions = emptyMap(),
37+
originalSource = null,
38+
)
39+
2940
fun fromFetchedValue(
3041
engineResult: Any?,
31-
fetchedValue: FetchedValue
42+
fetchedValue: FetchedValue,
43+
resolutionPolicy: ResolutionPolicy,
44+
originalSource: Any? = fetchedValue.fetchedValue,
3245
) = FieldResolutionResult(
3346
engineResult,
3447
fetchedValue.errors,
@@ -37,7 +50,8 @@ data class FieldResolutionResult(
3750
is FetchedValueWithExtensions -> fetchedValue.extensions
3851
else -> emptyMap()
3952
},
40-
fetchedValue.fetchedValue,
53+
originalSource,
54+
resolutionPolicy,
4155
)
4256
}
4357
}

engine/runtime/src/main/kotlin/viaduct/engine/runtime/execution/ExecutionParameters.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import viaduct.engine.api.FieldCheckerDispatcherRegistry
2626
import viaduct.engine.api.FieldResolverDispatcherRegistry
2727
import viaduct.engine.api.RawSelectionSet
2828
import viaduct.engine.api.RequiredSelectionSetRegistry
29+
import viaduct.engine.api.ResolutionPolicy
2930
import viaduct.engine.api.TypeCheckerDispatcherRegistry
3031
import viaduct.engine.api.gj
3132
import viaduct.engine.api.instrumentation.ViaductModernGJInstrumentation
@@ -58,6 +59,7 @@ import viaduct.utils.slf4j.logger
5859
* @property parent Parent parameters in the traversal chain, if any
5960
* @property field Field currently being executed, if any
6061
* @property bypassChecksDuringCompletion If execution is in the context of an access check
62+
* @property resolutionPolicy The resolution policy to use for this execution step
6163
*/
6264
data class ExecutionParameters(
6365
val constants: Constants,
@@ -71,7 +73,8 @@ data class ExecutionParameters(
7173
val errorAccumulator: ErrorAccumulator,
7274
val parent: ExecutionParameters? = null,
7375
val field: QueryPlan.CollectedField? = null,
74-
val bypassChecksDuringCompletion: Boolean = false
76+
val bypassChecksDuringCompletion: Boolean = false,
77+
val resolutionPolicy: ResolutionPolicy = ResolutionPolicy.STANDARD,
7578
) {
7679
// Computed properties
7780
/** The ResultPath for the current level of execution */
@@ -145,6 +148,7 @@ data class ExecutionParameters(
145148
field = field,
146149
executionStepInfo = executionStepInfo,
147150
parent = this,
151+
resolutionPolicy = resolutionPolicy,
148152
)
149153
}
150154

@@ -343,6 +347,7 @@ data class ExecutionParameters(
343347
parentEngineResult = newParentOER,
344348
localContext = localContext,
345349
source = source,
350+
resolutionPolicy = resolutionPolicy,
346351
)
347352
}
348353

@@ -360,6 +365,7 @@ data class ExecutionParameters(
360365
engineResult: ObjectEngineResultImpl,
361366
localContext: CompositeLocalContext,
362367
source: Any?,
368+
resolutionPolicy: ResolutionPolicy = this.resolutionPolicy,
363369
): ExecutionParameters {
364370
return copy(
365371
parentEngineResult = engineResult, // Update parent to be the current object we're traversing into
@@ -370,6 +376,7 @@ data class ExecutionParameters(
370376
localContext = localContext,
371377
source = source,
372378
selectionSet = checkNotNull(field.selectionSet) { "Expected selection set to be non-null." },
379+
resolutionPolicy = resolutionPolicy,
373380
)
374381
}
375382

engine/runtime/src/main/kotlin/viaduct/engine/runtime/execution/FieldCompleter.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import viaduct.engine.runtime.ObjectEngineResultImpl
2626
import viaduct.engine.runtime.ObjectEngineResultImpl.Companion.ACCESS_CHECK_SLOT
2727
import viaduct.engine.runtime.ObjectEngineResultImpl.Companion.RAW_VALUE_SLOT
2828
import viaduct.engine.runtime.Value
29-
import viaduct.engine.runtime.context.CompositeLocalContext
3029
import viaduct.engine.runtime.execution.CompletionErrors.FieldCompletionException
3130
import viaduct.engine.runtime.execution.FieldExecutionHelpers.buildDataFetchingEnvironment
3231
import viaduct.engine.runtime.execution.FieldExecutionHelpers.buildOERKeyForField
@@ -139,7 +138,7 @@ class FieldCompleter(
139138
// Handle fetch errors gracefully
140139
handleFetchingException(dataFetchingEnvironmentProvider, throwable)
141140
.map {
142-
FieldResolutionResult(null, it.errors, CompositeLocalContext.empty, emptyMap(), null)
141+
FieldResolutionResult.fromErrors(it.errors)
143142
}
144143
}
145144

engine/runtime/src/main/kotlin/viaduct/engine/runtime/execution/FieldExecutionHelpers.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ object FieldExecutionHelpers {
152152
val engineExecCtx = parameters.executionContext.findLocalContextForType<EngineExecutionContextImpl>()
153153
val fieldScope = EngineExecutionContextImpl.FieldExecutionScopeImpl(
154154
fragments = parameters.queryPlan.fragments.map.mapValues { it.value.gjDef },
155-
variables = parameters.coercedVariables.toMap()
155+
variables = parameters.coercedVariables.toMap(),
156+
resolutionPolicy = parameters.resolutionPolicy
156157
)
157158
val updatedEngineExecCtx = engineExecCtx.copy(fieldScope = fieldScope)
158159

engine/runtime/src/main/kotlin/viaduct/engine/runtime/execution/FieldResolver.kt

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import viaduct.deferred.asDeferred
2828
import viaduct.engine.api.CheckerResult
2929
import viaduct.engine.api.LazyEngineObjectData
3030
import viaduct.engine.api.ObjectEngineResult
31+
import viaduct.engine.api.ParentManagedValue
32+
import viaduct.engine.api.ResolutionPolicy
3133
import viaduct.engine.api.engineExecutionContext
3234
import viaduct.engine.runtime.Cell
3335
import viaduct.engine.runtime.EngineExecutionContextImpl
@@ -270,7 +272,7 @@ class FieldResolver(
270272
val (dataFetcherValue, fieldCheckerResultValue) = fetchField(field, parameters, dataFetchingEnvironmentProvider)
271273
val result = dataFetcherValue
272274
.map { fv ->
273-
buildFieldResolutionResult(parameters, fieldType, fv)
275+
buildFieldResolutionResult(parameters, fieldType, fv, parameters.resolutionPolicy)
274276
}.recover { e ->
275277
// handle any errors that occurred during building FieldResolutionResult
276278
val wrappedException = when (e) {
@@ -347,20 +349,34 @@ class FieldResolver(
347349
parameters: ExecutionParameters,
348350
fieldType: GraphQLOutputType,
349351
fetchedValue: FetchedValue,
352+
resolutionPolicy: ResolutionPolicy,
350353
): FieldResolutionResult {
351354
val field = checkNotNull(parameters.field) { "Expected parameters.field to be non-null." }
352-
val data = fetchedValue.fetchedValue ?: return FieldResolutionResult.fromFetchedValue(null, fetchedValue)
355+
val data = fetchedValue.fetchedValue ?: return FieldResolutionResult.fromFetchedValue(null, fetchedValue, resolutionPolicy)
356+
357+
// Unwrap data from "ParentManagedValue" if necessary, and set the effective resolution policy
358+
var effectiveResolutionPolicy = resolutionPolicy
359+
val effectiveData = if (data is ParentManagedValue) {
360+
effectiveResolutionPolicy = ResolutionPolicy.PARENT_MANAGED
361+
data.value
362+
} else {
363+
data
364+
}
365+
366+
if (effectiveData == null) {
367+
return FieldResolutionResult.fromFetchedValue(null, fetchedValue, effectiveResolutionPolicy)
368+
}
353369

354370
// if the type has a non-null wrapper, unwrap one level and recurse
355371
if (GraphQLTypeUtil.isNonNull(fieldType)) {
356-
return buildFieldResolutionResult(parameters, GraphQLTypeUtil.unwrapNonNullAs(fieldType), fetchedValue)
372+
return buildFieldResolutionResult(parameters, GraphQLTypeUtil.unwrapNonNullAs(fieldType), fetchedValue, effectiveResolutionPolicy)
357373
}
358374

359375
// When it's a list, wrap each item in the list
360376
if (GraphQLTypeUtil.isList(fieldType)) {
361377
val newFieldType = GraphQLTypeUtil.unwrapOneAs<GraphQLOutputType>(fieldType)
362-
val resultIterable = checkNotNull(data as? Iterable<*>) {
363-
"Expected data to be an Iterable, was ${data.javaClass}."
378+
val resultIterable = checkNotNull(effectiveData as? Iterable<*>) {
379+
"Expected data to be an Iterable, was ${effectiveData.javaClass}."
364380
}
365381
return FieldResolutionResult.fromFetchedValue(
366382
resultIterable.mapIndexed { index, it ->
@@ -370,7 +386,8 @@ class FieldResolver(
370386
val itemFieldResolutionResult = buildFieldResolutionResult(
371387
parameters,
372388
newFieldType,
373-
itemFV
389+
itemFV,
390+
effectiveResolutionPolicy
374391
)
375392
slotSetter.setRawValue(Value.fromValue(itemFieldResolutionResult))
376393

@@ -386,25 +403,27 @@ class FieldResolver(
386403
slotSetter.setCheckerValue(typeCheckerResult)
387404
}
388405
},
389-
fetchedValue
406+
fetchedValue,
407+
effectiveResolutionPolicy,
408+
originalSource = effectiveData,
390409
)
391410
}
392411

393412
// When it's a leaf value, it doesn't need wrapping
394413
if (GraphQLTypeUtil.isLeaf(fieldType)) {
395-
return FieldResolutionResult.fromFetchedValue(data, fetchedValue)
414+
return FieldResolutionResult.fromFetchedValue(effectiveData, fetchedValue, effectiveResolutionPolicy, originalSource = effectiveData)
396415
}
397416
// Interface or union type, resolve the type and wrap it
398417
if (GraphQLTypeUtil.isInterfaceOrUnion(fieldType)) {
399418
val resolvedType = typeResolver.resolveType(
400419
parameters.executionContext,
401420
field.mergedField,
402-
data,
421+
effectiveData,
403422
parameters.executionStepInfo,
404423
fieldType,
405424
fetchedValue.localContext
406425
)
407-
return buildFieldResolutionResult(parameters, resolvedType, fetchedValue)
426+
return buildFieldResolutionResult(parameters, resolvedType, fetchedValue, effectiveResolutionPolicy)
408427
}
409428
// When it's an object, wrap the whole thing
410429
if (GraphQLTypeUtil.isObjectType(fieldType)) {
@@ -413,7 +432,7 @@ class FieldResolver(
413432
} else {
414433
ObjectEngineResultImpl.newForType(fieldType as GraphQLObjectType)
415434
}
416-
return FieldResolutionResult.fromFetchedValue(oer, fetchedValue)
435+
return FieldResolutionResult.fromFetchedValue(oer, fetchedValue, effectiveResolutionPolicy, originalSource = effectiveData)
417436
}
418437
throw IllegalStateException("ObjectEngineResult must wrap a GraphQLObjectType.")
419438
}
@@ -522,7 +541,7 @@ class FieldResolver(
522541
val oer = fieldResolutionResult.engineResult as? ObjectEngineResultImpl ?: return
523542
fetchObject(
524543
oer.graphQLObjectType,
525-
parameters.forObjectTraversal(field, oer, fieldResolutionResult.localContext, fieldResolutionResult.originalSource)
544+
parameters.forObjectTraversal(field, oer, fieldResolutionResult.localContext, fieldResolutionResult.originalSource, fieldResolutionResult.resolutionPolicy)
526545
)
527546
}
528547
}

engine/runtime/src/main/kotlin/viaduct/engine/runtime/instrumentation/ResolverDataFetcherInstrumentation.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import graphql.schema.DataFetcher
66
import viaduct.engine.api.FieldCheckerDispatcherRegistry
77
import viaduct.engine.api.FieldResolverDispatcher
88
import viaduct.engine.api.FieldResolverDispatcherRegistry
9+
import viaduct.engine.api.ResolutionPolicy
10+
import viaduct.engine.api.ViaductDataFetchingEnvironment
911
import viaduct.engine.api.coroutines.CoroutineInterop
12+
import viaduct.engine.api.engineExecutionContext
1013
import viaduct.engine.api.instrumentation.ViaductModernGJInstrumentation
1114
import viaduct.engine.runtime.execution.DefaultCoroutineInterop
1215
import viaduct.engine.runtime.execution.ResolverDataFetcher
@@ -25,7 +28,13 @@ class ResolverDataFetcherInstrumentation(
2528
parameters: InstrumentationFieldFetchParameters,
2629
state: InstrumentationState?
2730
): DataFetcher<*> {
28-
val dfEnv = parameters.environment
31+
val dfEnv = parameters.environment as ViaductDataFetchingEnvironment
32+
33+
val resolutionPolicy = dfEnv.engineExecutionContext.fieldScope.resolutionPolicy
34+
if (resolutionPolicy == ResolutionPolicy.PARENT_MANAGED) {
35+
return dataFetcher
36+
}
37+
2938
val typeName = dfEnv.parentType.asNamedElement().name
3039
val fieldName = dfEnv.fieldDefinition.name
3140

0 commit comments

Comments
 (0)