Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/src/main/java/graphql/nadel/NadelExecutionHints.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import graphql.nadel.hints.AllDocumentVariablesHint
import graphql.nadel.hints.LegacyOperationNamesHint
import graphql.nadel.hints.NadelDeferSupportHint
import graphql.nadel.hints.NadelExecuteOnEngineSchemaHint
import graphql.nadel.hints.NadelHydrationExecutableSourceFields
import graphql.nadel.hints.NadelHydrationFilterObjectTypesHint
import graphql.nadel.hints.NadelSharedTypeRenamesHint
import graphql.nadel.hints.NadelShortCircuitEmptyQueryHint
Expand All @@ -20,6 +21,7 @@ data class NadelExecutionHints(
val virtualTypeSupport: NadelVirtualTypeSupportHint,
val executeOnEngineSchema: NadelExecuteOnEngineSchemaHint,
val hydrationFilterObjectTypes: NadelHydrationFilterObjectTypesHint,
val hydrationExecutableSourceFields: NadelHydrationExecutableSourceFields,
) {
/**
* Returns a builder with the same field values as this object.
Expand All @@ -41,16 +43,21 @@ data class NadelExecutionHints(
private var virtualTypeSupport = NadelVirtualTypeSupportHint { false }
private var executeOnEngineSchema = NadelExecuteOnEngineSchemaHint { false }
private var hydrationFilterObjectTypes = NadelHydrationFilterObjectTypesHint { false }
private var hydrationExecutableSourceFields = NadelHydrationExecutableSourceFields { false }

constructor()

constructor(nadelExecutionHints: NadelExecutionHints) {
legacyOperationNames = nadelExecutionHints.legacyOperationNames
allDocumentVariablesHint = nadelExecutionHints.allDocumentVariablesHint
newResultMergerAndNamespacedTypename = nadelExecutionHints.newResultMergerAndNamespacedTypename
deferSupport = nadelExecutionHints.deferSupport
shortCircuitEmptyQuery = nadelExecutionHints.shortCircuitEmptyQuery
sharedTypeRenames = nadelExecutionHints.sharedTypeRenames
virtualTypeSupport = nadelExecutionHints.virtualTypeSupport

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added missing copy.

executeOnEngineSchema = nadelExecutionHints.executeOnEngineSchema
hydrationFilterObjectTypes = nadelExecutionHints.hydrationFilterObjectTypes
hydrationExecutableSourceFields = nadelExecutionHints.hydrationExecutableSourceFields
}

fun legacyOperationNames(flag: LegacyOperationNamesHint): Builder {
Expand Down Expand Up @@ -98,6 +105,11 @@ data class NadelExecutionHints(
return this
}

fun hydrationExecutableSourceFields(flag: NadelHydrationExecutableSourceFields): Builder {
hydrationExecutableSourceFields = flag
return this
}

fun build(): NadelExecutionHints {
return NadelExecutionHints(
legacyOperationNames,
Expand All @@ -109,6 +121,7 @@ data class NadelExecutionHints(
virtualTypeSupport,
executeOnEngineSchema,
hydrationFilterObjectTypes,
hydrationExecutableSourceFields,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import graphql.nadel.engine.blueprint.hydration.NadelHydrationArgument
import graphql.nadel.engine.blueprint.hydration.NadelHydrationCondition
import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy
import graphql.nadel.engine.transform.query.NadelQueryPath
import graphql.normalized.ExecutableNormalizedField
import graphql.schema.FieldCoordinates
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLFieldsContainer
Expand Down Expand Up @@ -72,8 +73,16 @@ interface NadelGenericHydrationInstruction {
* This can be the fields described in [NadelHydrationArgument.ValueSource.FieldResultValue.queryPathToField]
* or [NadelBatchHydrationMatchStrategy.MatchObjectIdentifier.sourceId].
*/
@Deprecated("To be replaced by executableSourceFields")
val sourceFields: List<NadelQueryPath>

/**
* The fields required to be queried on the source object in order to complete the hydration.
* This can be the fields described in [NadelHydrationArgument.ValueSource.FieldResultValue.queryPathToField]
* or [NadelBatchHydrationMatchStrategy.MatchObjectIdentifier.sourceId].
*/
val executableSourceFields: List<ExecutableNormalizedField>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now build the entire ExecutableNormalizedField ahead of time instead of just computing the required NadelQueryPath


/**
* The field definition in the overall schema referenced by [queryPathToBackingField].
*/
Expand Down Expand Up @@ -107,7 +116,9 @@ data class NadelHydrationFieldInstruction(
override val queryPathToBackingField: NadelQueryPath,
override val backingFieldArguments: List<NadelHydrationArgument>,
override val timeout: Int,
@Deprecated("To be replaced by executableSourceFields")
override val sourceFields: List<NadelQueryPath>,
override val executableSourceFields: List<ExecutableNormalizedField>,
override val backingFieldDef: GraphQLFieldDefinition,
override val backingFieldContainer: GraphQLFieldsContainer,
override val backingFieldReturnsObjectTypeNames: Set<String>,
Expand All @@ -131,7 +142,9 @@ data class NadelBatchHydrationFieldInstruction(
override val queryPathToBackingField: NadelQueryPath,
override val backingFieldArguments: List<NadelHydrationArgument>,
override val timeout: Int,
@Deprecated("To be replaced by executableSourceFields")
override val sourceFields: List<NadelQueryPath>,
override val executableSourceFields: List<ExecutableNormalizedField>,
override val backingFieldDef: GraphQLFieldDefinition,
override val backingFieldContainer: GraphQLFieldsContainer,
override val backingFieldReturnsObjectTypeNames: Set<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,25 @@ internal object NadelHydrationFieldsBuilder {
}

fun makeRequiredSourceFields(
hints: NadelExecutionHints,
service: Service,
executionBlueprint: NadelOverallExecutionBlueprint,
aliasHelper: NadelAliasHelper,
objectTypeName: GraphQLObjectTypeName,
instructions: List<NadelGenericHydrationInstruction>,
): List<ExecutableNormalizedField> {
if (hints.hydrationExecutableSourceFields(service)) {
return instructions
.asSequence()
.flatMap {
it.executableSourceFields
}
.map {
aliasHelper.toArtificial(it)
}
.toList()
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New code here, the ENFs are built during validation time, now we just insert them from the instruction.


val underlyingTypeName = executionBlueprint.getUnderlyingTypeName(service, overallTypeName = objectTypeName)
val underlyingObjectType = service.underlyingSchema.getObjectType(underlyingTypeName)
?: error("No underlying object type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ internal class NadelHydrationTransform(
artificialFields = state.instructionsByObjectTypeNames
.flatMap { (typeName, instruction) ->
NadelHydrationFieldsBuilder.makeRequiredSourceFields(
hints = executionContext.hints,
service = service,
executionBlueprint = executionBlueprint,
aliasHelper = state.aliasHelper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ internal class NadelBatchHydrationTransform(
artificialFields = state.instructionsByObjectTypeNames
.flatMap { (objectTypeName, instructions) ->
NadelHydrationFieldsBuilder.makeRequiredSourceFields(
hints = executionContext.hints,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For FFing

service = service,
executionBlueprint = executionBlueprint,
aliasHelper = state.aliasHelper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ object NFUtil {
pathToFieldIndex: Int,
deferredExecutions: LinkedHashSet<NormalizedDeferredExecution>,
): ExecutableNormalizedField {
if(aliasedPath != null && aliasedPath.size != queryPathToField.size) {
if (aliasedPath != null && aliasedPath.size != queryPathToField.size) {
error("Aliased path must have the same length as the query path")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package graphql.nadel.hints

import graphql.nadel.Service

fun interface NadelHydrationExecutableSourceFields {
operator fun invoke(service: Service): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import graphql.nadel.validation.hydration.NadelHydrationArgumentTypeValidation
import graphql.nadel.validation.hydration.NadelHydrationArgumentValidation
import graphql.nadel.validation.hydration.NadelHydrationConditionValidation
import graphql.nadel.validation.hydration.NadelHydrationSourceFieldValidation
import graphql.nadel.validation.hydration.NadelHydrationSourceFieldValidation2
import graphql.nadel.validation.hydration.NadelHydrationValidation
import graphql.nadel.validation.hydration.NadelHydrationVirtualTypeValidation

Expand Down Expand Up @@ -48,6 +49,7 @@ abstract class NadelSchemaValidationFactory {
private val hydrationArgumentValidation = NadelHydrationArgumentValidation(hydrationArgumentTypeValidation)
private val hydrationConditionValidation = NadelHydrationConditionValidation()
private val hydrationSourceFieldValidation = NadelHydrationSourceFieldValidation()
private val hydrationSourceFieldValidation2 = NadelHydrationSourceFieldValidation2()
private val hydrationVirtualTypeValidation = NadelHydrationVirtualTypeValidation()

private val defaultHydrationDefinitionValidation = NadelDefaultHydrationDefinitionValidation()
Expand All @@ -56,6 +58,7 @@ abstract class NadelSchemaValidationFactory {
argumentValidation = hydrationArgumentValidation,
conditionValidation = hydrationConditionValidation,
sourceFieldValidation = hydrationSourceFieldValidation,
sourceFieldValidation2 = hydrationSourceFieldValidation2,
virtualTypeValidation = hydrationVirtualTypeValidation,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import graphql.nadel.validation.NadelValidationInterimResult.Success.Companion.a
import graphql.nadel.validation.onError
import graphql.schema.GraphQLObjectType

@Deprecated("To be replaced by NadelHydrationSourceFieldValidation2")
internal class NadelHydrationSourceFieldValidation {
context(NadelValidationContext)
fun getSourceFields(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package graphql.nadel.validation.hydration

import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy
import graphql.nadel.engine.blueprint.hydration.NadelHydrationArgument
import graphql.nadel.engine.blueprint.hydration.NadelHydrationCondition
import graphql.nadel.engine.transform.query.NFUtil
import graphql.nadel.engine.transform.query.NadelQueryPath
import graphql.nadel.engine.util.getFieldContainerFor
import graphql.nadel.engine.util.isNonNull
import graphql.nadel.engine.util.unwrapAll
import graphql.nadel.validation.NadelValidationContext
import graphql.nadel.validation.NadelValidationInterimResult
import graphql.nadel.validation.NadelValidationInterimResult.Success.Companion.asInterimSuccess
import graphql.nadel.validation.onError
import graphql.nadel.validation.onErrorCast
import graphql.normalized.ExecutableNormalizedField
import graphql.schema.GraphQLInputObjectType
import graphql.schema.GraphQLObjectType

/**
* Forked version of [NadelHydrationSourceFieldValidation] that has new functionality so we can feature flag the code.
*/
internal class NadelHydrationSourceFieldValidation2 {
context(NadelValidationContext, NadelHydrationValidationContext)
fun getSourceFields(
arguments: List<NadelHydrationArgument>,
hydrationCondition: NadelHydrationCondition?,
): NadelValidationInterimResult<List<ExecutableNormalizedField>> {
val argumentSourceFields = arguments.getSourceFields()
.onError { return it }
val conditionSourceFields = listOfNotNull(hydrationCondition?.fieldPath).map { makeLeafField(it) }

return (argumentSourceFields + conditionSourceFields)
.dedupSourceFields()
.asInterimSuccess()
}

context(NadelValidationContext, NadelHydrationValidationContext)
fun getBatchHydrationSourceFields(
arguments: List<NadelHydrationArgument>,
matchStrategy: NadelBatchHydrationMatchStrategy,
hydrationCondition: NadelHydrationCondition?,
): NadelValidationInterimResult<List<ExecutableNormalizedField>> {
val argumentSourceFields = arguments.getSourceFields()
.onError { return it }
val conditionSourceFields = listOfNotNull(hydrationCondition?.fieldPath).map { makeLeafField(it) }

return (argumentSourceFields + conditionSourceFields)
.dedupSourceFields()
.asInterimSuccess()
}

context(NadelValidationContext, NadelHydrationValidationContext)
private fun List<NadelHydrationArgument>.getSourceFields(): NadelValidationInterimResult<List<ExecutableNormalizedField>> {
return mapNotNull { argument ->
when (argument.valueSource) {
is NadelHydrationArgument.ValueSource.ArgumentValue -> null
is NadelHydrationArgument.ValueSource.FieldResultValue ->
getSourceFieldQueryPaths(argument, argument.valueSource)
.onErrorCast { return it }
is NadelHydrationArgument.ValueSource.StaticValue -> null
is NadelHydrationArgument.ValueSource.RemainingArguments -> null
}
}.asInterimSuccess()
}

context(NadelValidationContext, NadelHydrationValidationContext)
private fun getSourceFieldQueryPaths(
argument: NadelHydrationArgument,
hydrationValueSource: NadelHydrationArgument.ValueSource.FieldResultValue,
): NadelValidationInterimResult<ExecutableNormalizedField> {
val hydrationSourceType = hydrationValueSource.fieldDefinition.type.unwrapAll()

if (hydrationSourceType is GraphQLObjectType) {
return createObjectField(argument, hydrationValueSource)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main change, before this was

return hydrationSourceType.fields
.map { field ->
hydrationValueSource.queryPathToField.plus(field.name)
}
.asInterimSuccess()

Which only selected one level of children.

Now it iterates through the required input object fields and then creates the selection set from that. It does this recursively to create the correct selection set.

}

return makeLeafField(hydrationValueSource.queryPathToField)
.asInterimSuccess()
}

context(NadelValidationContext, NadelHydrationValidationContext)
private fun createObjectField(
argument: NadelHydrationArgument,
hydrationValueSource: NadelHydrationArgument.ValueSource.FieldResultValue,
): NadelValidationInterimResult<ExecutableNormalizedField> {
val parentObjectType =
parent.underlying.getFieldContainerFor(hydrationValueSource.queryPathToField.segments) as GraphQLObjectType

// todo: should probably check cardinality here too
val field = makeObjectField(
parentObjectType = parentObjectType,
fieldName = hydrationValueSource.fieldDefinition.name,
inputObjectType = argument.backingArgumentDef.type.unwrapAll() as GraphQLInputObjectType,
outputObjectType = hydrationValueSource.fieldDefinition.type.unwrapAll() as GraphQLObjectType,
)

return if (hydrationValueSource.queryPathToField.size > 1) {
NFUtil.createField(
schema = backingService.underlyingSchema,
parentType = parent.underlying as GraphQLObjectType,
queryPathToField = hydrationValueSource.queryPathToField.dropLast(1),
fieldArguments = emptyMap(),
fieldChildren = listOf(field),
)
} else {
field
}.asInterimSuccess()
}

private fun makeObjectField(
parentObjectType: GraphQLObjectType,
fieldName: String,
inputObjectType: GraphQLInputObjectType,
outputObjectType: GraphQLObjectType,
): ExecutableNormalizedField {
val children = inputObjectType.fields
.mapNotNull { inputField ->
val equivalentOutputField = outputObjectType.getField(inputField.name)
if (equivalentOutputField == null) {
if (inputField.type.isNonNull) { // i.e. required
error("Required input field is missed") // todo: proper error here
} else {
null
}
} else {
val parentObjectType = parentObjectType.getField(fieldName).type.unwrapAll() as GraphQLObjectType
if (
inputField.type.unwrapAll() is GraphQLInputObjectType
|| equivalentOutputField.type.unwrapAll() is GraphQLObjectType
) {
makeObjectField(
parentObjectType = parentObjectType,
fieldName = inputField.name,
inputObjectType = inputField.type.unwrapAll() as GraphQLInputObjectType,
outputObjectType = equivalentOutputField.type.unwrapAll() as GraphQLObjectType,
)
} else {
ExecutableNormalizedField.newNormalizedField()
.objectTypeNames(listOf(parentObjectType.name))
.fieldName(inputField.name)
.build()
}
}
}

return ExecutableNormalizedField.newNormalizedField()
.objectTypeNames(listOf(parentObjectType.name))
.fieldName(fieldName)
.children(children)
.build()
}

context(NadelValidationContext, NadelHydrationValidationContext)
private fun makeLeafField(
path: NadelQueryPath,
): ExecutableNormalizedField {
// todo: should do some validation here?? e.g. arg is a scalar value, type validation? maybe type validation is done elsewhere already
return NFUtil.createField(
schema = parent.service.underlyingSchema,
parentType = parent.underlying as GraphQLObjectType,
queryPathToField = path,
fieldArguments = emptyMap(),
fieldChildren = emptyList(), // This must be a leaf node
)
}
}

private fun List<ExecutableNormalizedField>.dedupSourceFields(): List<ExecutableNormalizedField> {
return groupBy {
listOf(it.objectTypeNames, it.resultKey, it.name, it.normalizedArguments.size)
}.flatMap { (_, fields) ->
if (fields.all { it.normalizedArguments.isEmpty() }) {
listOf(fields.first())
} else {
fields
}
}
}
Loading