Skip to content

Commit

Permalink
Basic support for virtual fields using hydration (#600)
Browse files Browse the repository at this point in the history
* Big hack

* Delete more hacks

* Replace hardcoded virtualType

* Delete dead code

* Delete unused comment

* Add extra types in test and ignore more virtual types in validation

* Add scalar test

* Address some PR comments

* Remove virtual type context from batch hydration

* Fix FF

* Add simple test

* Add failing test due to new FF

* Fix test

* Rename

* Delete unused

* Refactor

* Reformat

* Additional tests & FF
  • Loading branch information
gnawf authored Oct 31, 2024
1 parent f3833e1 commit f148c32
Show file tree
Hide file tree
Showing 31 changed files with 1,999 additions and 142 deletions.
9 changes: 9 additions & 0 deletions lib/src/main/java/graphql/nadel/NadelExecutionHints.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import graphql.nadel.hints.LegacyOperationNamesHint
import graphql.nadel.hints.NadelDeferSupportHint
import graphql.nadel.hints.NadelSharedTypeRenamesHint
import graphql.nadel.hints.NadelShortCircuitEmptyQueryHint
import graphql.nadel.hints.NadelVirtualTypeSupportHint
import graphql.nadel.hints.NewBatchHydrationGroupingHint
import graphql.nadel.hints.NewResultMergerAndNamespacedTypename

Expand All @@ -16,6 +17,7 @@ data class NadelExecutionHints(
val deferSupport: NadelDeferSupportHint,
val sharedTypeRenames: NadelSharedTypeRenamesHint,
val shortCircuitEmptyQuery: NadelShortCircuitEmptyQueryHint,
val virtualTypeSupport: NadelVirtualTypeSupportHint,
) {
/**
* Returns a builder with the same field values as this object.
Expand All @@ -35,6 +37,7 @@ data class NadelExecutionHints(
private var deferSupport = NadelDeferSupportHint { false }
private var shortCircuitEmptyQuery = NadelShortCircuitEmptyQueryHint { false }
private var sharedTypeRenames = NadelSharedTypeRenamesHint { false }
private var virtualTypeSupport = NadelVirtualTypeSupportHint { false }

constructor()

Expand Down Expand Up @@ -80,6 +83,11 @@ data class NadelExecutionHints(
return this
}

fun virtualTypeSupport(flag: NadelVirtualTypeSupportHint): Builder {
virtualTypeSupport = flag
return this
}

fun build(): NadelExecutionHints {
return NadelExecutionHints(
legacyOperationNames,
Expand All @@ -89,6 +97,7 @@ data class NadelExecutionHints(
deferSupport,
sharedTypeRenames,
shortCircuitEmptyQuery,
virtualTypeSupport,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graphql.nadel.engine.blueprint

import graphql.nadel.Service
import graphql.nadel.ServiceExecutionHydrationDetails
import graphql.nadel.engine.transform.GraphQLObjectTypeName
import graphql.nadel.engine.util.emptyOrSingle
import graphql.nadel.engine.util.makeFieldCoordinates
Expand Down Expand Up @@ -72,6 +73,41 @@ data class NadelOverallExecutionBlueprint(
return coordinatesToService[fieldCoordinates]
}

inline fun <reified T : NadelFieldInstruction> getInstructionInsideVirtualType(
hydrationDetails: ServiceExecutionHydrationDetails?,
backingField: ExecutableNormalizedField,
): Map<GraphQLObjectTypeName, List<T>> {
hydrationDetails ?: return emptyMap() // Need hydration to provide virtual hydration context

val backingFieldParentTypeName = backingField.objectTypeNames.singleOrNull()
?: return emptyMap() // Don't support abstract types for now

val nadelHydrationContext = fieldInstructions[hydrationDetails.hydrationSourceField]!!
.asSequence()
.filterIsInstance<NadelGenericHydrationInstruction>()
.first() as? NadelHydrationFieldInstruction
?: return emptyMap() // Virtual types only come about from standard hydrations, not batched

val virtualTypeContext = nadelHydrationContext.virtualTypeContext
?: return emptyMap() // Not all hydrations create virtual types

val virtualType = virtualTypeContext.backingTypeToVirtualType[backingFieldParentTypeName]
?: return emptyMap() // Not a virtual type

val fieldCoordinatesInVirtualType = makeFieldCoordinates(virtualType, backingField.name)

val instructions = fieldInstructions[fieldCoordinatesInVirtualType]
?.filterIsInstance<T>()
?.takeIf {
it.isNotEmpty()
}
?: return emptyMap()

return mapOf(
backingField.objectTypeNames.single() to instructions,
)
}

private fun getUnderlyingBlueprint(service: Service): NadelUnderlyingExecutionBlueprint {
val name = service.name
return underlyingBlueprints[name] ?: error("Could not find service: $name")
Expand Down Expand Up @@ -124,6 +160,9 @@ data class NadelTypeRenameInstructions internal constructor(
}
}

/**
* todo: why doesn't this belong inside [NadelOverallExecutionBlueprint]
*/
inline fun <reified T : NadelFieldInstruction> Map<FieldCoordinates, List<NadelFieldInstruction>>.getTypeNameToInstructionMap(
field: ExecutableNormalizedField,
): Map<GraphQLObjectTypeName, T> {
Expand All @@ -139,6 +178,9 @@ inline fun <reified T : NadelFieldInstruction> Map<FieldCoordinates, List<NadelF
)
}

/**
* todo: why doesn't this belong inside [NadelOverallExecutionBlueprint]
*/
inline fun <reified T : NadelFieldInstruction> Map<FieldCoordinates, List<NadelFieldInstruction>>.getTypeNameToInstructionsMap(
field: ExecutableNormalizedField,
): Map<GraphQLObjectTypeName, List<T>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import graphql.nadel.dsl.FieldMappingDefinition
import graphql.nadel.dsl.NadelHydrationDefinition
import graphql.nadel.dsl.RemoteArgumentSource
import graphql.nadel.dsl.TypeMappingDefinition
import graphql.nadel.engine.blueprint.directives.isVirtualType
import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy
import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef
import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef.ValueSource.FieldResultValue
Expand Down Expand Up @@ -60,6 +61,7 @@ private class Factory(
) {
private val definitionNamesToService: Map<String, Service> = makeDefinitionNamesToService()
private val coordinatesToService: Map<FieldCoordinates, Service> = makeCoordinatesToService()
private val virtualTypeBlueprintFactory = NadelVirtualTypeBlueprintFactory()

fun make(): NadelOverallExecutionBlueprint {
val typeRenameInstructions = makeTypeRenameInstructions().strictAssociateBy {
Expand Down Expand Up @@ -259,6 +261,11 @@ private class Factory(
actorFieldDef = actorFieldDef,
actorInputValueDefs = hydrationArgs,
),
virtualTypeContext = virtualTypeBlueprintFactory.makeVirtualTypeContext(
engineSchema = engineSchema,
containerType = hydratedFieldParentType,
virtualFieldDef = hydratedFieldDef
),
sourceFields = getHydrationSourceFields(hydrationArgs, condition),
condition = condition,
)
Expand Down Expand Up @@ -328,9 +335,14 @@ private class Factory(
return@mapNotNull null
}

val underlyingParentType = getUnderlyingType(hydratedFieldParentType, hydratedFieldDef)
?: error("No underlying type for: ${hydratedFieldParentType.name}")
val fieldDefs = underlyingParentType.getFieldsAlong(inputValueDef.valueSource.queryPathToField.segments)
val typeToLookAt = if (hydratedFieldParentType.isVirtualType()) {
hydratedFieldParentType
} else {
getUnderlyingType(hydratedFieldParentType, hydratedFieldDef)
?: error("No underlying type for: ${hydratedFieldParentType.name}")
}

val fieldDefs = typeToLookAt.getFieldsAlong(inputValueDef.valueSource.queryPathToField.segments)
inputValueDef.takeIf {
fieldDefs.any { fieldDef ->
fieldDef.type.unwrapNonNull().isList
Expand Down Expand Up @@ -552,10 +564,17 @@ private class Factory(
)
}
is RemoteArgumentSource.ObjectField -> {
// Ugh code still uses underlying schema, we need to pull these up to the overall schema
val typeToLookAt = if (hydratedFieldParentType.isVirtualType()) {
hydratedFieldParentType
} else {
getUnderlyingType(hydratedFieldParentType, hydratedFieldDef)
}

val pathToField = argSourceType.pathToField
FieldResultValue(
queryPathToField = NadelQueryPath(pathToField),
fieldDefinition = getUnderlyingType(hydratedFieldParentType, hydratedFieldDef)
fieldDefinition = typeToLookAt
?.getFieldAt(pathToField)
?: error("No field defined at: ${hydratedFieldParentType.name}.${pathToField.joinToString(".")}"),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package graphql.nadel.engine.blueprint
import graphql.nadel.Service
import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy
import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef
import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy
import graphql.nadel.engine.blueprint.hydration.NadelHydrationCondition
import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy
import graphql.nadel.engine.transform.query.NadelQueryPath
import graphql.schema.FieldCoordinates
import graphql.schema.GraphQLFieldDefinition
Expand Down Expand Up @@ -102,6 +102,14 @@ data class NadelHydrationFieldInstruction(
override val actorFieldDef: GraphQLFieldDefinition,
override val actorFieldContainer: GraphQLFieldsContainer,
override val condition: NadelHydrationCondition?,
/**
* Hydration can bring about virtual types.
*
* These types mirror the backing type, but can have things like hydration etc.
*
* This context provides a mapping between the virtual and backing types.
*/
val virtualTypeContext: NadelVirtualTypeContext?,
val hydrationStrategy: NadelHydrationStrategy,
) : NadelFieldInstruction(), NadelGenericHydrationInstruction

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package graphql.nadel.engine.blueprint

import graphql.nadel.engine.blueprint.directives.getHydratedOrNull
import graphql.nadel.engine.blueprint.directives.isHydration
import graphql.nadel.engine.util.getFieldAt
import graphql.nadel.engine.util.unwrapAll
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLObjectType
import graphql.schema.GraphQLSchema

internal class NadelVirtualTypeBlueprintFactory {
private data class FactoryContext(
val engineSchema: GraphQLSchema,
)

fun makeVirtualTypeContext(
engineSchema: GraphQLSchema,
containerType: GraphQLObjectType,
virtualFieldDef: GraphQLFieldDefinition,
): NadelVirtualTypeContext? {
val factoryContext = FactoryContext(
engineSchema,
)

return with(factoryContext) {
makeVirtualTypeContext(containerType, virtualFieldDef)
}
}

context(FactoryContext)
private fun makeVirtualTypeContext(
containerType: GraphQLObjectType,
virtualFieldDef: GraphQLFieldDefinition,
): NadelVirtualTypeContext? {
val typeMappings = createTypeMappings(virtualFieldDef)

return if (typeMappings.isEmpty()) {
return null
} else {
NadelVirtualTypeContext(
virtualFieldContainer = containerType,
virtualField = virtualFieldDef,
virtualTypeToBackingType = typeMappings.virtualTypeToBackingTypeMap(),
backingTypeToVirtualType = typeMappings.backingTypeToVirtualTypeMap(),
)
}
}

context(FactoryContext)
private fun createTypeMappings(
virtualFieldDef: GraphQLFieldDefinition,
): List<VirtualTypeMapping> {
val hydration = virtualFieldDef.getHydratedOrNull()
?: return emptyList() // We should use the non-null method once we delete @hydratedFrom
val backingFieldDef = engineSchema.queryType.getFieldAt(hydration.backingField)!!
val backingType = backingFieldDef.type.unwrapAll() as? GraphQLObjectType
?: return emptyList()
val virtualType = virtualFieldDef.type.unwrapAll() as? GraphQLObjectType
?: return emptyList()

// Not a virtual type
if (virtualType.name == backingType.name) {
return emptyList()
}

return createTypeMappings(
backingType = backingType,
virtualType = virtualType,
)
}

context(FactoryContext)
private fun createTypeMappings(
backingType: GraphQLObjectType,
virtualType: GraphQLObjectType,
): List<VirtualTypeMapping> {
return listOf(VirtualTypeMapping(virtualType, backingType)) + createTypeMappings(
virtualType = virtualType,
backingType = backingType,
visitedVirtualTypes = mutableSetOf(virtualType.name),
)
}

context(FactoryContext)
private fun createTypeMappings(
virtualType: GraphQLObjectType,
backingType: GraphQLObjectType,
visitedVirtualTypes: MutableSet<String>,
): List<VirtualTypeMapping> {
return virtualType
.fields
.flatMap { virtualFieldDef ->
val virtualOutputType = virtualFieldDef.type.unwrapAll() as? GraphQLObjectType
?: return@flatMap emptyList()

if (visitedVirtualTypes.visit(virtualOutputType.name)) {
val backingFieldDef = backingType.getField(virtualFieldDef.name)
val backingOutputType = backingFieldDef.type.unwrapAll() as GraphQLObjectType

// Recursively create type mapping
val childTypeMappings = createTypeMappings(
visitedVirtualTypes = visitedVirtualTypes,
virtualType = virtualOutputType,
backingType = backingOutputType,
)

listOf(VirtualTypeMapping(virtualOutputType, backingOutputType)) + childTypeMappings
} else {
emptyList()
}
}
}

data class VirtualTypeMapping(
val virtualType: GraphQLObjectType,
val backingType: GraphQLObjectType,
)

/**
* Util to create [Map]
*/
private fun List<VirtualTypeMapping>.virtualTypeToBackingTypeMap(): Map<String, String> {
val mapping = mutableMapOf<String, String>()
forEach {
mapping[it.virtualType.name] = it.backingType.name
}
return mapping
}

/**
* Util to create [Map]
*/
private fun List<VirtualTypeMapping>.backingTypeToVirtualTypeMap(): Map<String, String> {
val mapping = mutableMapOf<String, String>()
forEach {
mapping[it.backingType.name] = it.virtualType.name
}
return mapping
}
}

/**
* @return true if the element hasn't been visited before.
* Also mutates the [MutableSet] to mark it as visited.
*/
private fun MutableSet<String>.visit(element: String): Boolean {
return if (contains(element)) {
false
} else {
add(element)
true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package graphql.nadel.engine.blueprint

import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLFieldsContainer

data class NadelVirtualTypeContext(
/**
* The container of the virtual field that created this mapping.
*/
val virtualFieldContainer: GraphQLFieldsContainer,
/**
* The virtual field that created this mapping.
*/
val virtualField: GraphQLFieldDefinition,
val virtualTypeToBackingType: Map<String, String>,
val backingTypeToVirtualType: Map<String, String>,
)
Loading

0 comments on commit f148c32

Please sign in to comment.