graphql.nadel.engine.transform.skipInclude.NadelSkipIncludeTransform.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nadel Show documentation
Show all versions of nadel Show documentation
Nadel is a Java library that combines multiple GrahpQL services together into one API.
package graphql.nadel.engine.transform.skipInclude
import graphql.execution.MergedField
import graphql.introspection.Introspection
import graphql.language.Field
import graphql.nadel.Service
import graphql.nadel.ServiceExecutionHydrationDetails
import graphql.nadel.ServiceExecutionResult
import graphql.nadel.engine.NadelExecutionContext
import graphql.nadel.engine.blueprint.NadelOverallExecutionBlueprint
import graphql.nadel.engine.transform.NadelTransform
import graphql.nadel.engine.transform.NadelTransformFieldResult
import graphql.nadel.engine.transform.artificial.NadelAliasHelper
import graphql.nadel.engine.transform.query.NadelQueryTransformer
import graphql.nadel.engine.transform.result.NadelResultInstruction
import graphql.nadel.engine.transform.result.json.JsonNodes
import graphql.nadel.engine.transform.skipInclude.NadelSkipIncludeTransform.State
import graphql.nadel.engine.util.resolveObjectTypes
import graphql.nadel.engine.util.toBuilder
import graphql.normalized.ExecutableNormalizedField
import graphql.normalized.ExecutableNormalizedField.newNormalizedField
import graphql.schema.GraphQLSchema
/**
* So the way `@skip` and `@include` work is that in the [graphql.normalized.ExecutableNormalizedOperationFactory]
* is that they get automatically removed by the factory. Because of that, we never actually
* get the fields. This causes bad outcomes for Nadel because we don't execute the query here.
* We forward the query and we are _not_ allowed generate invalid queries with empty
* selection sets.
*
* So here, we add back in a `__typename` field to ensure we don't have an empty selection.
*
* This should probably be a more generic "if no subselections add an empty one for removed fields".
* But we'll deal with that separately.
*/
internal class NadelSkipIncludeTransform : NadelTransform {
companion object {
private const val skipFieldName = "__skip"
fun isSkipIncludeSpecialField(enf: ExecutableNormalizedField): Boolean {
return enf.name == skipFieldName
}
}
class State(
val aliasHelper: NadelAliasHelper,
)
/**
* So this transform is a bit odd. Normally transform operate on a specific field.
*
* However, in the case of a `@skip` the field with that directive is automatically removed.
*
* So in order to execute on the deleted field we insert a fake field back into the midst for
* the transform API to pick up on.
*
* This should really not happen. The real fix is to execute on the parent of the deleted
* field and to fix [getResultInstructions] to include `underlyingField` and not just `underlyingParentField`.
*/
override suspend fun isApplicable(
executionContext: NadelExecutionContext,
executionBlueprint: NadelOverallExecutionBlueprint,
services: Map,
service: Service,
overallField: ExecutableNormalizedField,
hydrationDetails: ServiceExecutionHydrationDetails?,
): State? {
// This hacks together a child that will pass through here
if (overallField.children.isEmpty()) {
val mergedField = executionContext.query.getMergedField(overallField)
if (hasAnyChildren(mergedField)) {
// Adds a field so we can transform it
overallField.children.add(createSkipField(executionBlueprint.engineSchema, overallField))
}
}
return if (overallField.name == skipFieldName) {
State(
aliasHelper = NadelAliasHelper.forField(
tag = "skip_include",
field = overallField,
),
)
} else {
null
}
}
override suspend fun transformField(
executionContext: NadelExecutionContext,
transformer: NadelQueryTransformer,
executionBlueprint: NadelOverallExecutionBlueprint,
service: Service,
field: ExecutableNormalizedField,
state: State,
): NadelTransformFieldResult {
return NadelTransformFieldResult(
newField = null,
artificialFields = listOf(
field.toBuilder()
.alias(state.aliasHelper.typeNameResultKey)
.fieldName(Introspection.TypeNameMetaFieldDef.name)
.build(),
),
)
}
override suspend fun getResultInstructions(
executionContext: NadelExecutionContext,
executionBlueprint: NadelOverallExecutionBlueprint,
service: Service,
overallField: ExecutableNormalizedField,
underlyingParentField: ExecutableNormalizedField?,
result: ServiceExecutionResult,
state: State,
nodes: JsonNodes,
): List {
return emptyList()
}
private fun hasAnyChildren(mergedField: MergedField?): Boolean {
return mergedField?.fields?.any(::hasAnyChildren) == true
}
private fun hasAnyChildren(field: Field?): Boolean {
return field?.selectionSet?.selections?.isNotEmpty() == true
}
private fun createSkipField(
overallSchema: GraphQLSchema,
parent: ExecutableNormalizedField,
): ExecutableNormalizedField {
val objectTypeNames = parent.getFieldDefinitions(overallSchema)
.asSequence()
.map {
it.type
}
.flatMap {
// This resolves abstract types to object types
resolveObjectTypes(overallSchema, it) { type ->
// Interface always resolves to object types
// Unions MUST contain object types https://spec.graphql.org/draft/#sec-Unions.Type-Validation
error("Unable to resolve to object type: $type")
}
}
.map {
it.name
}
.toList()
return newNormalizedField()
.objectTypeNames(objectTypeNames)
.fieldName(skipFieldName)
.build()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy