Skip to content

Commit 8edc55f

Browse files
committed
Filter objectTypeNames for non-batched hydration
1 parent a76601b commit 8edc55f

13 files changed

Lines changed: 600 additions & 48 deletions

lib/src/main/java/graphql/nadel/NadelExecutionHints.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import graphql.nadel.hints.AllDocumentVariablesHint
44
import graphql.nadel.hints.LegacyOperationNamesHint
55
import graphql.nadel.hints.NadelDeferSupportHint
66
import graphql.nadel.hints.NadelExecuteOnEngineSchemaHint
7+
import graphql.nadel.hints.NadelHydrationFilterObjectTypesHint
78
import graphql.nadel.hints.NadelSharedTypeRenamesHint
89
import graphql.nadel.hints.NadelShortCircuitEmptyQueryHint
910
import graphql.nadel.hints.NadelVirtualTypeSupportHint
@@ -18,6 +19,7 @@ data class NadelExecutionHints(
1819
val shortCircuitEmptyQuery: NadelShortCircuitEmptyQueryHint,
1920
val virtualTypeSupport: NadelVirtualTypeSupportHint,
2021
val executeOnEngineSchema: NadelExecuteOnEngineSchemaHint,
22+
val hydrationFilterObjectTypes: NadelHydrationFilterObjectTypesHint,
2123
) {
2224
/**
2325
* Returns a builder with the same field values as this object.
@@ -38,6 +40,7 @@ data class NadelExecutionHints(
3840
private var sharedTypeRenames = NadelSharedTypeRenamesHint { false }
3941
private var virtualTypeSupport = NadelVirtualTypeSupportHint { false }
4042
private var executeOnEngineSchema = NadelExecuteOnEngineSchemaHint { false }
43+
private var hydrationFilterObjectTypes = NadelHydrationFilterObjectTypesHint { false }
4144

4245
constructor()
4346

@@ -47,6 +50,7 @@ data class NadelExecutionHints(
4750
newResultMergerAndNamespacedTypename = nadelExecutionHints.newResultMergerAndNamespacedTypename
4851
shortCircuitEmptyQuery = nadelExecutionHints.shortCircuitEmptyQuery
4952
executeOnEngineSchema = nadelExecutionHints.executeOnEngineSchema
53+
hydrationFilterObjectTypes = nadelExecutionHints.hydrationFilterObjectTypes
5054
}
5155

5256
fun legacyOperationNames(flag: LegacyOperationNamesHint): Builder {
@@ -89,6 +93,11 @@ data class NadelExecutionHints(
8993
return this
9094
}
9195

96+
fun hydrationFilterObjectTypes(flag: NadelHydrationFilterObjectTypesHint): Builder {
97+
hydrationFilterObjectTypes = flag
98+
return this
99+
}
100+
92101
fun build(): NadelExecutionHints {
93102
return NadelExecutionHints(
94103
legacyOperationNames,
@@ -98,7 +107,8 @@ data class NadelExecutionHints(
98107
sharedTypeRenames,
99108
shortCircuitEmptyQuery,
100109
virtualTypeSupport,
101-
executeOnEngineSchema
110+
executeOnEngineSchema,
111+
hydrationFilterObjectTypes,
102112
)
103113
}
104114
}

lib/src/main/java/graphql/nadel/engine/NadelExecutionContext.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import graphql.nadel.hooks.NadelExecutionHooks
1010
import graphql.nadel.result.NadelResultTracker
1111
import graphql.normalized.ExecutableNormalizedOperation
1212
import kotlinx.coroutines.CoroutineScope
13-
import java.util.concurrent.CompletableFuture
14-
import java.util.concurrent.ConcurrentHashMap
1513

1614
data class NadelExecutionContext internal constructor(
1715
val executionInput: ExecutionInput,
@@ -26,8 +24,6 @@ data class NadelExecutionContext internal constructor(
2624
internal val isPartitionedCall: Boolean = false,
2725
internal val executionCoroutine: CoroutineScope,
2826
) {
29-
private val serviceContexts = ConcurrentHashMap<String, CompletableFuture<Any?>>()
30-
3127
val userContext: Any?
3228
get() {
3329
return executionInput.context

lib/src/main/java/graphql/nadel/engine/blueprint/NadelFieldInstruction.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ interface NadelGenericHydrationInstruction {
7979
*/
8080
val backingFieldDef: GraphQLFieldDefinition
8181

82+
/**
83+
* The names of the object types the backing field returns.
84+
*
85+
* Note: when virtual types are involved these are the virtual types.
86+
*
87+
* Could be empty if there are no object types involved e.g. returns scalar, enum etc.
88+
*/
89+
val backingFieldReturnsObjectTypeNames: Set<String>
90+
8291
/**
8392
* The container of the backing field in the overall schema referenced by [queryPathToBackingField].
8493
*/
@@ -101,6 +110,7 @@ data class NadelHydrationFieldInstruction(
101110
override val sourceFields: List<NadelQueryPath>,
102111
override val backingFieldDef: GraphQLFieldDefinition,
103112
override val backingFieldContainer: GraphQLFieldsContainer,
113+
override val backingFieldReturnsObjectTypeNames: Set<String>,
104114
override val condition: NadelHydrationCondition?,
105115
/**
106116
* Hydration can bring about virtual types.
@@ -124,6 +134,7 @@ data class NadelBatchHydrationFieldInstruction(
124134
override val sourceFields: List<NadelQueryPath>,
125135
override val backingFieldDef: GraphQLFieldDefinition,
126136
override val backingFieldContainer: GraphQLFieldsContainer,
137+
override val backingFieldReturnsObjectTypeNames: Set<String>,
127138
override val condition: NadelHydrationCondition?,
128139
val batchSize: Int,
129140
val batchHydrationMatchStrategy: NadelBatchHydrationMatchStrategy,

lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationFieldsBuilder.kt

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package graphql.nadel.engine.transform.hydration
22

3+
import graphql.nadel.NadelExecutionHints
34
import graphql.nadel.Service
45
import graphql.nadel.engine.NadelExecutionContext
56
import graphql.nadel.engine.blueprint.NadelBatchHydrationFieldInstruction
@@ -40,7 +41,13 @@ internal object NadelHydrationFieldsBuilder {
4041
makeBackingQueries(
4142
instruction = instruction,
4243
fieldArguments = args,
43-
fieldChildren = deepClone(virtualField.children),
44+
fieldChildren = deepClone(
45+
if (executionContext.hints.hydrationFilterObjectTypes()) {
46+
filterChildren(instruction, virtualField.children)
47+
} else {
48+
virtualField.children
49+
}
50+
),
4451
executionBlueprint = executionBlueprint,
4552
)
4653
}
@@ -56,30 +63,37 @@ internal object NadelHydrationFieldsBuilder {
5663
}
5764

5865
fun makeBatchBackingQueries(
66+
executionHints: NadelExecutionHints,
5967
executionBlueprint: NadelOverallExecutionBlueprint,
6068
instruction: NadelBatchHydrationFieldInstruction,
6169
aliasHelper: NadelAliasHelper,
6270
virtualField: ExecutableNormalizedField,
6371
argBatches: List<Map<NadelHydrationArgument, NormalizedInputValue>>,
6472
): List<ExecutableNormalizedField> {
65-
val backingFieldOverallObjectTypeNames = getBackingFieldOverallObjectTypenames(instruction, executionBlueprint)
66-
val fieldChildren = deepClone(fields = virtualField.children)
67-
.mapNotNull { childField ->
68-
val objectTypesAreNotReturnedByBackingField =
69-
backingFieldOverallObjectTypeNames.none { it in childField.objectTypeNames }
73+
val fieldChildren = if (executionHints.hydrationFilterObjectTypes()) {
74+
deepClone(fields = filterChildren(instruction, virtualField.children)) +
75+
makeObjectIdFields(executionBlueprint, aliasHelper, instruction)
76+
} else {
77+
val backingFieldOverallObjectTypeNames =
78+
getBackingFieldOverallObjectTypenames(instruction, executionBlueprint)
79+
deepClone(fields = virtualField.children)
80+
.mapNotNull { childField ->
81+
val objectTypesAreNotReturnedByBackingField =
82+
backingFieldOverallObjectTypeNames.none { it in childField.objectTypeNames }
7083

71-
if (objectTypesAreNotReturnedByBackingField) {
72-
null
73-
} else {
74-
childField.toBuilder()
75-
.clearObjectTypesNames()
76-
.objectTypeNames(childField.objectTypeNames.filter { it in backingFieldOverallObjectTypeNames })
77-
.build()
84+
if (objectTypesAreNotReturnedByBackingField) {
85+
null
86+
} else {
87+
childField.toBuilder()
88+
.clearObjectTypesNames()
89+
.objectTypeNames(childField.objectTypeNames.filter { it in backingFieldOverallObjectTypeNames })
90+
.build()
91+
}
7892
}
79-
}
80-
.let { children ->
81-
children + makeObjectIdFields(executionBlueprint, aliasHelper, instruction)
82-
}
93+
.let { children ->
94+
children + makeObjectIdFields(executionBlueprint, aliasHelper, instruction)
95+
}
96+
}
8397

8498
return argBatches.map { argBatch ->
8599
makeBackingQueries(
@@ -91,6 +105,29 @@ internal object NadelHydrationFieldsBuilder {
91105
}
92106
}
93107

108+
private fun filterChildren(
109+
instruction: NadelGenericHydrationInstruction,
110+
children: List<ExecutableNormalizedField>,
111+
): List<ExecutableNormalizedField> {
112+
return children
113+
.mapNotNull { childField ->
114+
val legalObjectTypeNames =
115+
childField.objectTypeNames.intersect(instruction.backingFieldReturnsObjectTypeNames)
116+
117+
if (legalObjectTypeNames.isEmpty()) {
118+
null
119+
} else if (legalObjectTypeNames == childField.objectTypeNames) {
120+
childField
121+
} else {
122+
childField.toBuilder()
123+
.clearObjectTypesNames()
124+
.objectTypeNames(legalObjectTypeNames.toList())
125+
.build()
126+
}
127+
}
128+
}
129+
130+
@Deprecated("Will be removed once NadelHydrationFilterObjectTypesHint is rolled out")
94131
private fun getBackingFieldOverallObjectTypenames(
95132
instruction: NadelBatchHydrationFieldInstruction,
96133
executionBlueprint: NadelOverallExecutionBlueprint,

lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelNewBatchHydrator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import graphql.nadel.engine.transform.getInstructionsForNode
1616
import graphql.nadel.engine.transform.hydration.NadelHydrationFieldsBuilder
1717
import graphql.nadel.engine.transform.hydration.NadelHydrationUtil.getInstructionsToAddErrors
1818
import graphql.nadel.engine.transform.hydration.batch.NadelBatchHydrationTransform.State
19-
import graphql.nadel.engine.transform.hydration.batch.NadelNewBatchHydrator.SourceObjectMetadata
2019
import graphql.nadel.engine.transform.hydration.batch.indexing.NadelBatchHydrationIndexBasedIndexer
2120
import graphql.nadel.engine.transform.hydration.batch.indexing.NadelBatchHydrationIndexKey
2221
import graphql.nadel.engine.transform.hydration.batch.indexing.NadelBatchHydrationIndexer
@@ -444,6 +443,7 @@ internal class NadelNewBatchHydrator(
444443

445444
val queries = NadelHydrationFieldsBuilder
446445
.makeBatchBackingQueries(
446+
executionHints = executionContext.hints,
447447
executionBlueprint = executionBlueprint,
448448
instruction = instruction,
449449
aliasHelper = aliasHelper,

lib/src/main/java/graphql/nadel/engine/util/GraphQLUtil.kt

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,11 @@ import graphql.schema.GraphQLCodeRegistry
6060
import graphql.schema.GraphQLFieldDefinition
6161
import graphql.schema.GraphQLFieldsContainer
6262
import graphql.schema.GraphQLInputType
63-
import graphql.schema.GraphQLInterfaceType
6463
import graphql.schema.GraphQLNamedType
6564
import graphql.schema.GraphQLObjectType
6665
import graphql.schema.GraphQLSchema
6766
import graphql.schema.GraphQLType
6867
import graphql.schema.GraphQLTypeUtil
69-
import graphql.schema.GraphQLUnionType
7068
import graphql.schema.GraphQLUnmodifiedType
7169
import graphql.schema.idl.TypeUtil
7270
import kotlinx.coroutines.future.asDeferred
@@ -525,20 +523,31 @@ internal suspend fun NadelInstrumentation.beginExecute(
525523
* Turns GraphQL types to object types when possible e.g. finds concrete implementations
526524
* for interfaces, gets object types inside unions, and returns objects as is.
527525
*/
528-
fun resolveObjectTypes(
526+
inline fun resolveObjectTypes(
529527
schema: GraphQLSchema,
530528
type: GraphQLType,
531529
onNotObjectType: (GraphQLType) -> Nothing,
532530
): List<GraphQLObjectType> {
533-
return when (val unwrappedType = type.unwrapAll()) {
534-
is GraphQLObjectType -> listOf(unwrappedType)
535-
is GraphQLUnionType -> unwrappedType.types.flatMap {
536-
resolveObjectTypes(schema, type = it, onNotObjectType)
537-
}
538-
539-
is GraphQLInterfaceType -> schema.getImplementations(unwrappedType)
540-
else -> onNotObjectType(unwrappedType)
541-
}
531+
return type.unwrapAll()
532+
.whenType(
533+
enumType = onNotObjectType,
534+
inputObjectType = onNotObjectType,
535+
interfaceType = { interfaceType ->
536+
schema.getImplementations(interfaceType)
537+
.map { objectImpl -> objectImpl }
538+
},
539+
objectType = {
540+
listOf(it)
541+
},
542+
scalarType = onNotObjectType,
543+
unionType = { unionType ->
544+
unionType.types
545+
.map { memberType ->
546+
memberType as? GraphQLObjectType
547+
?: throw IllegalArgumentException("Member type must be object but found ${memberType?.javaClass?.name}")
548+
}
549+
},
550+
)
542551
}
543552

544553
/**
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package graphql.nadel.hints
2+
3+
fun interface NadelHydrationFilterObjectTypesHint {
4+
operator fun invoke(): Boolean
5+
}

lib/src/main/java/graphql/nadel/validation/hydration/NadelHydrationValidation.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import graphql.nadel.definition.hydration.NadelHydrationDefinition
99
import graphql.nadel.definition.virtualType.hasVirtualTypeDefinition
1010
import graphql.nadel.engine.blueprint.NadelBatchHydrationFieldInstruction
1111
import graphql.nadel.engine.blueprint.NadelHydrationFieldInstruction
12+
import graphql.nadel.engine.blueprint.NadelVirtualTypeContext
1213
import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy
1314
import graphql.nadel.engine.blueprint.hydration.NadelHydrationArgument
1415
import graphql.nadel.engine.blueprint.hydration.NadelHydrationCondition
@@ -25,6 +26,7 @@ import graphql.nadel.engine.util.isList
2526
import graphql.nadel.engine.util.isNonNull
2627
import graphql.nadel.engine.util.makeFieldCoordinates
2728
import graphql.nadel.engine.util.partitionCount
29+
import graphql.nadel.engine.util.resolveObjectTypes
2830
import graphql.nadel.engine.util.singleOfTypeOrNull
2931
import graphql.nadel.engine.util.startsWith
3032
import graphql.nadel.engine.util.unwrapAll
@@ -279,6 +281,7 @@ class NadelHydrationValidation internal constructor(
279281
timeout = hydrationDefinition.timeout,
280282
sourceFields = sourceFields,
281283
backingFieldDef = backingField,
284+
backingFieldReturnsObjectTypeNames = getReturnsObjectTypeNames(backingField, virtualTypeContext),
282285
backingFieldContainer = backingFieldContainer,
283286
condition = hydrationCondition,
284287
virtualTypeContext = virtualTypeContext,
@@ -356,6 +359,7 @@ class NadelHydrationValidation internal constructor(
356359
sourceFields = sourceFields,
357360
backingFieldDef = backingField,
358361
backingFieldContainer = backingFieldContainer,
362+
backingFieldReturnsObjectTypeNames = getReturnsObjectTypeNames(backingField, null),
359363
condition = hydrationCondition,
360364
batchSize = hydrationDefinition.batchSize,
361365
batchHydrationMatchStrategy = matchStrategy,
@@ -744,4 +748,21 @@ class NadelHydrationValidation internal constructor(
744748
}
745749
return false
746750
}
751+
752+
context(NadelValidationContext)
753+
private fun getReturnsObjectTypeNames(
754+
backingField: GraphQLFieldDefinition,
755+
virtualTypeContext: NadelVirtualTypeContext?,
756+
): Set<String> {
757+
val returnsObjectTypes = resolveObjectTypes(engineSchema, backingField.type) { return emptySet() }
758+
.mapTo(mutableSetOf()) { it.name }
759+
760+
return if (virtualTypeContext == null) {
761+
returnsObjectTypes
762+
} else {
763+
returnsObjectTypes.mapTo(mutableSetOf()) {
764+
virtualTypeContext.backingTypeToVirtualType[it] ?: it
765+
}
766+
}
767+
}
747768
}

test/src/test/kotlin/graphql/nadel/tests/legacy/new hydration/polymorphic hydrations/solitary polymorphic hydration snapshot.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ public class `solitary polymorphic hydration snapshot` : TestSnapshot() {
6464
| {
6565
| humanById(id: "HUMAN-0") {
6666
| __typename
67-
| __typename__type_filter__id: __typename
68-
| __typename__type_filter__breed: __typename
6967
| id
7068
| name
7169
| }
@@ -77,9 +75,7 @@ public class `solitary polymorphic hydration snapshot` : TestSnapshot() {
7775
| "data": {
7876
| "humanById": {
7977
| "__typename": "Human",
80-
| "__typename__type_filter__id": "Human",
8178
| "id": "HUMAN-0",
82-
| "__typename__type_filter__breed": "Human",
8379
| "name": "Fanny Longbottom"
8480
| }
8581
| }
@@ -94,8 +90,6 @@ public class `solitary polymorphic hydration snapshot` : TestSnapshot() {
9490
| {
9591
| petById(id: "PET-0") {
9692
| __typename
97-
| __typename__type_filter__id: __typename
98-
| __typename__type_filter__name: __typename
9993
| breed
10094
| id
10195
| }
@@ -108,9 +102,7 @@ public class `solitary polymorphic hydration snapshot` : TestSnapshot() {
108102
| "petById": {
109103
| "__typename": "Pet",
110104
| "id": "PET-0",
111-
| "__typename__type_filter__id": "Pet",
112-
| "breed": "Akita",
113-
| "__typename__type_filter__name": "Pet"
105+
| "breed": "Akita"
114106
| }
115107
| }
116108
| }

test/src/test/kotlin/graphql/nadel/tests/legacy/new hydration/polymorphic hydrations/solitary polymorphic hydration when hook returns null snapshot.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ public class `solitary polymorphic hydration when hook returns null snapshot` :
6464
| {
6565
| humanById(id: "HUMAN-0") {
6666
| __typename
67-
| __typename__type_filter__id: __typename
68-
| __typename__type_filter__breed: __typename
6967
| id
7068
| name
7169
| }
@@ -77,9 +75,7 @@ public class `solitary polymorphic hydration when hook returns null snapshot` :
7775
| "data": {
7876
| "humanById": {
7977
| "__typename": "Human",
80-
| "__typename__type_filter__id": "Human",
8178
| "id": "HUMAN-0",
82-
| "__typename__type_filter__breed": "Human",
8379
| "name": "Fanny Longbottom"
8480
| }
8581
| }

0 commit comments

Comments
 (0)