Skip to content

Commit f835bab

Browse files
committed
Executable source fields
1 parent 6f26e6b commit f835bab

12 files changed

Lines changed: 494 additions & 28 deletions

File tree

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

Lines changed: 13 additions & 0 deletions
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.NadelHydrationExecutableSourceFields
78
import graphql.nadel.hints.NadelHydrationFilterObjectTypesHint
89
import graphql.nadel.hints.NadelSharedTypeRenamesHint
910
import graphql.nadel.hints.NadelShortCircuitEmptyQueryHint
@@ -20,6 +21,7 @@ data class NadelExecutionHints(
2021
val virtualTypeSupport: NadelVirtualTypeSupportHint,
2122
val executeOnEngineSchema: NadelExecuteOnEngineSchemaHint,
2223
val hydrationFilterObjectTypes: NadelHydrationFilterObjectTypesHint,
24+
val hydrationExecutableSourceFields: NadelHydrationExecutableSourceFields,
2325
) {
2426
/**
2527
* Returns a builder with the same field values as this object.
@@ -41,16 +43,21 @@ data class NadelExecutionHints(
4143
private var virtualTypeSupport = NadelVirtualTypeSupportHint { false }
4244
private var executeOnEngineSchema = NadelExecuteOnEngineSchemaHint { false }
4345
private var hydrationFilterObjectTypes = NadelHydrationFilterObjectTypesHint { false }
46+
private var hydrationExecutableSourceFields = NadelHydrationExecutableSourceFields { false }
4447

4548
constructor()
4649

4750
constructor(nadelExecutionHints: NadelExecutionHints) {
4851
legacyOperationNames = nadelExecutionHints.legacyOperationNames
4952
allDocumentVariablesHint = nadelExecutionHints.allDocumentVariablesHint
5053
newResultMergerAndNamespacedTypename = nadelExecutionHints.newResultMergerAndNamespacedTypename
54+
deferSupport = nadelExecutionHints.deferSupport
5155
shortCircuitEmptyQuery = nadelExecutionHints.shortCircuitEmptyQuery
56+
sharedTypeRenames = nadelExecutionHints.sharedTypeRenames
57+
virtualTypeSupport = nadelExecutionHints.virtualTypeSupport
5258
executeOnEngineSchema = nadelExecutionHints.executeOnEngineSchema
5359
hydrationFilterObjectTypes = nadelExecutionHints.hydrationFilterObjectTypes
60+
hydrationExecutableSourceFields = nadelExecutionHints.hydrationExecutableSourceFields
5461
}
5562

5663
fun legacyOperationNames(flag: LegacyOperationNamesHint): Builder {
@@ -98,6 +105,11 @@ data class NadelExecutionHints(
98105
return this
99106
}
100107

108+
fun hydrationExecutableSourceFields(flag: NadelHydrationExecutableSourceFields): Builder {
109+
hydrationExecutableSourceFields = flag
110+
return this
111+
}
112+
101113
fun build(): NadelExecutionHints {
102114
return NadelExecutionHints(
103115
legacyOperationNames,
@@ -109,6 +121,7 @@ data class NadelExecutionHints(
109121
virtualTypeSupport,
110122
executeOnEngineSchema,
111123
hydrationFilterObjectTypes,
124+
hydrationExecutableSourceFields,
112125
)
113126
}
114127
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import graphql.nadel.engine.blueprint.hydration.NadelHydrationArgument
66
import graphql.nadel.engine.blueprint.hydration.NadelHydrationCondition
77
import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy
88
import graphql.nadel.engine.transform.query.NadelQueryPath
9+
import graphql.normalized.ExecutableNormalizedField
910
import graphql.schema.FieldCoordinates
1011
import graphql.schema.GraphQLFieldDefinition
1112
import graphql.schema.GraphQLFieldsContainer
@@ -74,6 +75,8 @@ interface NadelGenericHydrationInstruction {
7475
*/
7576
val sourceFields: List<NadelQueryPath>
7677

78+
val executableSourceFields: List<ExecutableNormalizedField>
79+
7780
/**
7881
* The field definition in the overall schema referenced by [queryPathToBackingField].
7982
*/
@@ -108,6 +111,7 @@ data class NadelHydrationFieldInstruction(
108111
override val backingFieldArguments: List<NadelHydrationArgument>,
109112
override val timeout: Int,
110113
override val sourceFields: List<NadelQueryPath>,
114+
override val executableSourceFields: List<ExecutableNormalizedField>,
111115
override val backingFieldDef: GraphQLFieldDefinition,
112116
override val backingFieldContainer: GraphQLFieldsContainer,
113117
override val backingFieldReturnsObjectTypeNames: Set<String>,
@@ -132,6 +136,7 @@ data class NadelBatchHydrationFieldInstruction(
132136
override val backingFieldArguments: List<NadelHydrationArgument>,
133137
override val timeout: Int,
134138
override val sourceFields: List<NadelQueryPath>,
139+
override val executableSourceFields: List<ExecutableNormalizedField>,
135140
override val backingFieldDef: GraphQLFieldDefinition,
136141
override val backingFieldContainer: GraphQLFieldsContainer,
137142
override val backingFieldReturnsObjectTypeNames: Set<String>,

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

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -148,31 +148,44 @@ internal object NadelHydrationFieldsBuilder {
148148
}
149149

150150
fun makeRequiredSourceFields(
151+
hints: NadelExecutionHints,
151152
service: Service,
152153
executionBlueprint: NadelOverallExecutionBlueprint,
153154
aliasHelper: NadelAliasHelper,
154155
objectTypeName: GraphQLObjectTypeName,
155156
instructions: List<NadelGenericHydrationInstruction>,
156157
): List<ExecutableNormalizedField> {
157-
val underlyingTypeName = executionBlueprint.getUnderlyingTypeName(service, overallTypeName = objectTypeName)
158-
val underlyingObjectType = service.underlyingSchema.getObjectType(underlyingTypeName)
159-
?: error("No underlying object type")
160-
161-
return instructions
162-
.asSequence()
163-
.flatMap { it.sourceFields }
164-
.map {
165-
aliasHelper.toArtificial(
166-
NFUtil.createField(
167-
schema = service.underlyingSchema,
168-
parentType = underlyingObjectType,
169-
queryPathToField = it,
170-
fieldArguments = emptyMap(),
171-
fieldChildren = emptyList(), // This must be a leaf node
172-
),
173-
)
174-
}
175-
.toList()
158+
if (hints.hydrationExecutableSourceFields()) {
159+
return instructions
160+
.asSequence()
161+
.flatMap {
162+
it.executableSourceFields
163+
}
164+
.map {
165+
aliasHelper.toArtificial(it)
166+
}
167+
.toList()
168+
} else {
169+
val underlyingTypeName = executionBlueprint.getUnderlyingTypeName(service, overallTypeName = objectTypeName)
170+
val underlyingObjectType = service.underlyingSchema.getObjectType(underlyingTypeName)
171+
?: error("No underlying object type")
172+
173+
return instructions
174+
.asSequence()
175+
.flatMap { it.sourceFields }
176+
.map {
177+
aliasHelper.toArtificial(
178+
NFUtil.createField(
179+
schema = service.underlyingSchema,
180+
parentType = underlyingObjectType,
181+
queryPathToField = it,
182+
fieldArguments = emptyMap(),
183+
fieldChildren = emptyList(), // This must be a leaf node
184+
),
185+
)
186+
}
187+
.toList()
188+
}
176189
}
177190

178191
private fun makeBackingQueries(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ internal class NadelHydrationTransform(
123123
artificialFields = state.instructionsByObjectTypeNames
124124
.flatMap { (typeName, instruction) ->
125125
NadelHydrationFieldsBuilder.makeRequiredSourceFields(
126+
hints = executionContext.hints,
126127
service = service,
127128
executionBlueprint = executionBlueprint,
128129
aliasHelper = state.aliasHelper,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ internal class NadelBatchHydrationTransform(
9393
artificialFields = state.instructionsByObjectTypeNames
9494
.flatMap { (objectTypeName, instructions) ->
9595
NadelHydrationFieldsBuilder.makeRequiredSourceFields(
96+
hints = executionContext.hints,
9697
service = service,
9798
executionBlueprint = executionBlueprint,
9899
aliasHelper = state.aliasHelper,

lib/src/main/java/graphql/nadel/engine/transform/query/NFUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ object NFUtil {
106106
pathToFieldIndex: Int,
107107
deferredExecutions: LinkedHashSet<NormalizedDeferredExecution>,
108108
): ExecutableNormalizedField {
109-
if(aliasedPath != null && aliasedPath.size != queryPathToField.size) {
109+
if (aliasedPath != null && aliasedPath.size != queryPathToField.size) {
110110
error("Aliased path must have the same length as the query path")
111111
}
112112

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 NadelHydrationExecutableSourceFields {
4+
operator fun invoke(): Boolean
5+
}

lib/src/main/java/graphql/nadel/validation/NadelSchemaValidationFactory.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import graphql.nadel.validation.hydration.NadelHydrationArgumentTypeValidation
55
import graphql.nadel.validation.hydration.NadelHydrationArgumentValidation
66
import graphql.nadel.validation.hydration.NadelHydrationConditionValidation
77
import graphql.nadel.validation.hydration.NadelHydrationSourceFieldValidation
8+
import graphql.nadel.validation.hydration.NadelHydrationSourceFieldValidation2
89
import graphql.nadel.validation.hydration.NadelHydrationValidation
910
import graphql.nadel.validation.hydration.NadelHydrationVirtualTypeValidation
1011

@@ -48,6 +49,7 @@ abstract class NadelSchemaValidationFactory {
4849
private val hydrationArgumentValidation = NadelHydrationArgumentValidation(hydrationArgumentTypeValidation)
4950
private val hydrationConditionValidation = NadelHydrationConditionValidation()
5051
private val hydrationSourceFieldValidation = NadelHydrationSourceFieldValidation()
52+
private val hydrationSourceFieldValidation2 = NadelHydrationSourceFieldValidation2()
5153
private val hydrationVirtualTypeValidation = NadelHydrationVirtualTypeValidation()
5254

5355
private val defaultHydrationDefinitionValidation = NadelDefaultHydrationDefinitionValidation()
@@ -56,6 +58,7 @@ abstract class NadelSchemaValidationFactory {
5658
argumentValidation = hydrationArgumentValidation,
5759
conditionValidation = hydrationConditionValidation,
5860
sourceFieldValidation = hydrationSourceFieldValidation,
61+
sourceFieldValidation2 = hydrationSourceFieldValidation2,
5962
virtualTypeValidation = hydrationVirtualTypeValidation,
6063
)
6164

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package graphql.nadel.validation.hydration
2+
3+
import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy
4+
import graphql.nadel.engine.blueprint.hydration.NadelHydrationArgument
5+
import graphql.nadel.engine.blueprint.hydration.NadelHydrationCondition
6+
import graphql.nadel.engine.transform.query.NFUtil
7+
import graphql.nadel.engine.transform.query.NadelQueryPath
8+
import graphql.nadel.engine.util.getFieldContainerFor
9+
import graphql.nadel.engine.util.isNonNull
10+
import graphql.nadel.engine.util.unwrapAll
11+
import graphql.nadel.validation.NadelValidationContext
12+
import graphql.nadel.validation.NadelValidationInterimResult
13+
import graphql.nadel.validation.NadelValidationInterimResult.Success.Companion.asInterimSuccess
14+
import graphql.nadel.validation.onError
15+
import graphql.nadel.validation.onErrorCast
16+
import graphql.normalized.ExecutableNormalizedField
17+
import graphql.schema.GraphQLInputObjectType
18+
import graphql.schema.GraphQLObjectType
19+
20+
/**
21+
* Forked version of [NadelHydrationSourceFieldValidation] that has new functionality so we can feature flag the code.
22+
*/
23+
internal class NadelHydrationSourceFieldValidation2 {
24+
context(NadelValidationContext, NadelHydrationValidationContext)
25+
fun getSourceFields(
26+
arguments: List<NadelHydrationArgument>,
27+
hydrationCondition: NadelHydrationCondition?,
28+
): NadelValidationInterimResult<List<ExecutableNormalizedField>> {
29+
val argumentSourceFields = arguments.getSourceFields()
30+
.onError { return it }
31+
val conditionSourceFields = listOfNotNull(hydrationCondition?.fieldPath).map { makeLeafField(it) }
32+
33+
return (argumentSourceFields + conditionSourceFields)
34+
.asInterimSuccess()
35+
}
36+
37+
context(NadelValidationContext, NadelHydrationValidationContext)
38+
fun getBatchHydrationSourceFields(
39+
arguments: List<NadelHydrationArgument>,
40+
matchStrategy: NadelBatchHydrationMatchStrategy,
41+
hydrationCondition: NadelHydrationCondition?,
42+
): NadelValidationInterimResult<List<ExecutableNormalizedField>> {
43+
val argumentSourceFields = arguments.getSourceFields()
44+
.onError { return it }
45+
val conditionSourceFields = listOfNotNull(hydrationCondition?.fieldPath).map { makeLeafField(it) }
46+
47+
return (argumentSourceFields + conditionSourceFields)
48+
.asInterimSuccess()
49+
}
50+
51+
context(NadelValidationContext, NadelHydrationValidationContext)
52+
private fun List<NadelHydrationArgument>.getSourceFields(): NadelValidationInterimResult<List<ExecutableNormalizedField>> {
53+
return mapNotNull { argument ->
54+
when (argument.valueSource) {
55+
is NadelHydrationArgument.ValueSource.ArgumentValue -> null
56+
is NadelHydrationArgument.ValueSource.FieldResultValue ->
57+
getSourceFieldQueryPaths(argument, argument.valueSource)
58+
.onErrorCast { return it }
59+
is NadelHydrationArgument.ValueSource.StaticValue -> null
60+
is NadelHydrationArgument.ValueSource.RemainingArguments -> null
61+
}
62+
}.asInterimSuccess()
63+
}
64+
65+
context(NadelValidationContext, NadelHydrationValidationContext)
66+
private fun getSourceFieldQueryPaths(
67+
argument: NadelHydrationArgument,
68+
hydrationValueSource: NadelHydrationArgument.ValueSource.FieldResultValue,
69+
): NadelValidationInterimResult<ExecutableNormalizedField> {
70+
val hydrationSourceType = hydrationValueSource.fieldDefinition.type.unwrapAll()
71+
72+
if (hydrationSourceType is GraphQLObjectType) {
73+
return createObjectField(argument, hydrationValueSource)
74+
}
75+
76+
return makeLeafField(hydrationValueSource.queryPathToField)
77+
.asInterimSuccess()
78+
}
79+
80+
context(NadelValidationContext, NadelHydrationValidationContext)
81+
private fun createObjectField(
82+
argument: NadelHydrationArgument,
83+
hydrationValueSource: NadelHydrationArgument.ValueSource.FieldResultValue,
84+
): NadelValidationInterimResult<ExecutableNormalizedField> {
85+
val parentObjectType =
86+
parent.underlying.getFieldContainerFor(hydrationValueSource.queryPathToField.segments) as GraphQLObjectType
87+
88+
// todo: need to handle parents
89+
// todo: should probably check cardinality here too
90+
return makeObjectField(
91+
parentObjectType = parentObjectType,
92+
fieldName = hydrationValueSource.fieldDefinition.name,
93+
inputObjectType = argument.backingArgumentDef.type.unwrapAll() as GraphQLInputObjectType,
94+
outputObjectType = hydrationValueSource.fieldDefinition.type.unwrapAll() as GraphQLObjectType,
95+
).asInterimSuccess()
96+
}
97+
98+
private fun makeObjectField(
99+
parentObjectType: GraphQLObjectType,
100+
fieldName: String,
101+
inputObjectType: GraphQLInputObjectType,
102+
outputObjectType: GraphQLObjectType,
103+
): ExecutableNormalizedField {
104+
val children = inputObjectType.fields
105+
.mapNotNull { inputField ->
106+
val equivalentOutputField = outputObjectType.getField(inputField.name)
107+
if (equivalentOutputField == null) {
108+
if (inputField.type.isNonNull) { // i.e. required
109+
null
110+
} else {
111+
error("Required input field is missed") // todo: proper error here
112+
}
113+
} else {
114+
val parentObjectType = parentObjectType.getField(fieldName).type.unwrapAll() as GraphQLObjectType
115+
if (
116+
inputField.type.unwrapAll() is GraphQLInputObjectType
117+
|| equivalentOutputField.type.unwrapAll() is GraphQLObjectType
118+
) {
119+
makeObjectField(
120+
parentObjectType = parentObjectType,
121+
fieldName = inputField.name,
122+
inputObjectType = inputField.type.unwrapAll() as GraphQLInputObjectType,
123+
outputObjectType = equivalentOutputField.type.unwrapAll() as GraphQLObjectType,
124+
)
125+
} else {
126+
ExecutableNormalizedField.newNormalizedField()
127+
.objectTypeNames(listOf(parentObjectType.name))
128+
.fieldName(inputField.name)
129+
.build()
130+
}
131+
}
132+
}
133+
134+
return ExecutableNormalizedField.newNormalizedField()
135+
.objectTypeNames(listOf(parentObjectType.name))
136+
.fieldName(fieldName)
137+
.children(children)
138+
.build()
139+
}
140+
141+
context(NadelValidationContext, NadelHydrationValidationContext)
142+
private fun makeLeafField(
143+
path: NadelQueryPath,
144+
): ExecutableNormalizedField {
145+
// todo: should do some validation here?? e.g. arg is a scalar value, type validation? maybe type validation is done elsewhere already
146+
return NFUtil.createField(
147+
schema = backingService.underlyingSchema,
148+
parentType = parent.underlying as GraphQLObjectType,
149+
queryPathToField = path,
150+
fieldArguments = emptyMap(),
151+
fieldChildren = emptyList(), // This must be a leaf node
152+
)
153+
}
154+
}

0 commit comments

Comments
 (0)