Skip to content

Commit

Permalink
Default hydration validation (#668)
Browse files Browse the repository at this point in the history
  • Loading branch information
gnawf authored Jan 21, 2025
1 parent 602de38 commit efee18f
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graphql.nadel.definition.hydration

import graphql.language.DirectiveDefinition
import graphql.nadel.definition.NadelInstructionDefinition
import graphql.nadel.definition.hydration.NadelDefaultHydrationDefinition.Keyword
import graphql.nadel.engine.util.parseDefinition
import graphql.schema.GraphQLAppliedDirective
Expand All @@ -11,14 +12,14 @@ fun GraphQLNamedType.hasDefaultHydration(): Boolean {
return (this as? GraphQLDirectiveContainer)?.hasAppliedDirective(Keyword.defaultHydration) == true
}

fun GraphQLNamedType.getDefaultHydrationOrNull(): NadelDefaultHydrationDefinition? {
fun GraphQLNamedType.parseDefaultHydrationOrNull(): NadelDefaultHydrationDefinition? {
return (this as? GraphQLDirectiveContainer)?.getAppliedDirective(Keyword.defaultHydration)
?.let(::NadelDefaultHydrationDefinition)
}

class NadelDefaultHydrationDefinition(
private val appliedDirective: GraphQLAppliedDirective,
) {
) : NadelInstructionDefinition {
companion object {
val directiveDefinition = parseDefinition<DirectiveDefinition>(
// language=GraphQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import graphql.nadel.definition.hydration.NadelHydrationArgumentDefinition
import graphql.nadel.definition.hydration.NadelHydrationConditionDefinition
import graphql.nadel.definition.hydration.NadelHydrationDefinition
import graphql.nadel.definition.hydration.NadelIdHydrationDefinition
import graphql.nadel.definition.hydration.getDefaultHydrationOrNull
import graphql.nadel.definition.hydration.parseDefaultHydrationOrNull
import graphql.nadel.definition.hydration.parseIdHydrationOrNull
import graphql.nadel.engine.util.unwrapAll
import graphql.schema.GraphQLFieldDefinition
Expand Down Expand Up @@ -61,7 +61,7 @@ internal class NadelIdHydrationDefinitionParser {
return NadelValidationInterimResult.Success.of(
virtualFieldType.types
.mapNotNull { unionMemberType ->
if ((unionMemberType as? GraphQLNamedType)?.getDefaultHydrationOrNull() == null) {
if ((unionMemberType as? GraphQLNamedType)?.parseDefaultHydrationOrNull() == null) {
null
} else {
getHydrationDefinitionForType(
Expand All @@ -81,7 +81,7 @@ internal class NadelIdHydrationDefinitionParser {
idHydration: NadelIdHydrationDefinition,
type: GraphQLNamedType,
): NadelValidationInterimResult<NadelHydrationDefinition> {
val defaultHydration = (type as? GraphQLNamedType)?.getDefaultHydrationOrNull()
val defaultHydration = (type as? GraphQLNamedType)?.parseDefaultHydrationOrNull()
?: return NadelValidationInterimResult.Error.of(NadelMissingDefaultHydrationError(parent, virtualField))

return NadelValidationInterimResult.Success.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package graphql.nadel.validation
import graphql.nadel.definition.NadelInstructionDefinition
import graphql.nadel.definition.NadelSchemaMemberCoordinates
import graphql.nadel.definition.coordinates
import graphql.nadel.definition.hydration.parseDefaultHydrationOrNull
import graphql.nadel.definition.hydration.parseHydrationDefinitions
import graphql.nadel.definition.partition.parsePartitionOrNull
import graphql.nadel.definition.renamed.parseRenamedOrNull
Expand Down Expand Up @@ -165,12 +166,16 @@ internal class NadelInstructionDefinitionParser(
val coordinates = element.coordinates()
val type = element.node

type.parseRenamedOrNull()?.also { renamed ->
definitionMap.computeIfAbsent(coordinates) { mutableListOf() }.add(renamed)
val definitions by lazy {
definitionMap.computeIfAbsent(coordinates) {
mutableListOf()
}
}

type.parseRenamedOrNull()?.also(definitions::add)
type.parseDefaultHydrationOrNull()?.also(definitions::add)
if (type.hasVirtualTypeDefinition()) {
definitionMap.computeIfAbsent(coordinates) { mutableListOf() }
.add(NadelVirtualTypeDefinition())
definitions.add(NadelVirtualTypeDefinition())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package graphql.nadel.validation
import graphql.nadel.definition.NadelInstructionDefinition
import graphql.nadel.definition.NadelSchemaMemberCoordinates
import graphql.nadel.definition.coordinates
import graphql.nadel.definition.hydration.NadelDefaultHydrationDefinition
import graphql.nadel.definition.hydration.NadelHydrationDefinition
import graphql.nadel.definition.partition.NadelPartitionDefinition
import graphql.nadel.definition.renamed.NadelRenamedDefinition
import graphql.nadel.engine.util.emptyOrSingle
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLFieldsContainer
import graphql.schema.GraphQLNamedType
Expand All @@ -24,24 +26,24 @@ data class NadelInstructionDefinitionRegistry(
container: NadelServiceSchemaElement.FieldsContainer,
field: GraphQLFieldDefinition,
): Boolean {
return getHydrationDefinitions(coords(container, field)).any()
return getHydrationDefinitionSequence(coords(container, field)).any()
}

fun getHydrationDefinitions(
container: NadelServiceSchemaElement.FieldsContainer,
field: GraphQLFieldDefinition,
): Sequence<NadelHydrationDefinition> {
return getHydrationDefinitions(coords(container, field))
return getHydrationDefinitionSequence(coords(container, field))
}

fun getHydrationDefinitions(
container: GraphQLFieldsContainer,
field: GraphQLFieldDefinition,
): Sequence<NadelHydrationDefinition> {
return getHydrationDefinitions(container.coordinates().field(field.name))
return getHydrationDefinitionSequence(container.coordinates().field(field.name))
}

fun getHydrationDefinitions(
fun getHydrationDefinitionSequence(
coords: NadelSchemaMemberCoordinates.Field,
): Sequence<NadelHydrationDefinition> {
val definitions = definitions[coords]
Expand Down Expand Up @@ -154,6 +156,29 @@ data class NadelInstructionDefinitionRegistry(
.firstOrNull()
}

fun hasDefaultHydration(
type: NadelServiceSchemaElement.Type,
): Boolean {
return getDefaultHydrationSequence(type.overall.coordinates()).any()
}

fun getDefaultHydrationOrNull(
type: NadelServiceSchemaElement.Type,
): NadelDefaultHydrationDefinition? {
return getDefaultHydrationSequence(type.overall.coordinates()).emptyOrSingle()
}

private fun getDefaultHydrationSequence(
coords: NadelSchemaMemberCoordinates.Type,
): Sequence<NadelDefaultHydrationDefinition> {
val definitions = definitions[coords]
?: return emptySequence()

return definitions
.asSequence()
.filterIsInstance<NadelDefaultHydrationDefinition>()
}

private fun coords(
container: NadelServiceSchemaElement.FieldsContainer,
field: GraphQLFieldDefinition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,48 @@ data class NadelMissingDefaultHydrationError(
override val subject = virtualField
}

data class NadelDefaultHydrationFieldNotFoundError(
val type: NadelServiceSchemaElement.Type,
val defaultHydration: NadelDefaultHydrationDefinition,
) : NadelSchemaValidationError {
override val message = run {
val typeName = type.overall.name
val field = defaultHydration.backingField.joinToString(".")
"Type $typeName declares @defaultHydration which references field $field that does not exist"
}

override val subject = type.overall
}

data class NadelDefaultHydrationIncompatibleBackingFieldTypeError(
val type: NadelServiceSchemaElement.Type,
val defaultHydration: NadelDefaultHydrationDefinition,
val backingField: GraphQLFieldDefinition,
) : NadelSchemaValidationError {
override val message = run {
val typeName = type.overall.name
val field = defaultHydration.backingField.joinToString(".")
"Type $typeName declares @defaultHydration backed by $field but that field does not return $typeName"
}

override val subject = type.overall
}

data class NadelDefaultHydrationIdArgumentNotFoundError(
val type: NadelServiceSchemaElement.Type,
val defaultHydration: NadelDefaultHydrationDefinition,
val backingField: GraphQLFieldDefinition,
) : NadelSchemaValidationError {
override val message = run {
val typeName = type.overall.name
val field = defaultHydration.backingField.joinToString(".")
val idArgument = defaultHydration.idArgument
"Type $typeName declares @defaultHydration backed by field $field but it is missing specified $idArgument argument"
}

override val subject = type.overall
}

data class NadelPolymorphicHydrationIncompatibleSourceFieldsError(
val parentType: NadelServiceSchemaElement,
val virtualField: GraphQLFieldDefinition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,7 @@ import graphql.schema.GraphQLSchema
import graphql.schema.GraphQLUnionType

class NadelSchemaValidation internal constructor(
private val fieldValidation: NadelFieldValidation,
private val inputValidation: NadelInputObjectValidation,
private val unionValidation: NadelUnionValidation,
private val enumValidation: NadelEnumValidation,
private val interfaceValidation: NadelInterfaceValidation,
private val namespaceValidation: NadelNamespaceValidation,
private val virtualTypeValidation: NadelVirtualTypeValidation,
private val typeValidation: NadelTypeValidation,
private val instructionDefinitionParser: NadelInstructionDefinitionParser,
private val hook: NadelSchemaValidationHook,
) {
Expand Down Expand Up @@ -67,16 +61,6 @@ class NadelSchemaValidation internal constructor(
hook = hook,
)

val typeValidation = NadelTypeValidation(
fieldValidation = fieldValidation,
inputValidation = inputValidation,
unionValidation = unionValidation,
enumValidation = enumValidation,
interfaceValidation = interfaceValidation,
namespaceValidation = namespaceValidation,
virtualTypeValidation = virtualTypeValidation,
)

return with(context) {
services
.map {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package graphql.nadel.validation

import graphql.nadel.validation.hydration.NadelDefaultHydrationDefinitionValidation
import graphql.nadel.validation.hydration.NadelHydrationArgumentTypeValidation
import graphql.nadel.validation.hydration.NadelHydrationArgumentValidation
import graphql.nadel.validation.hydration.NadelHydrationConditionValidation
Expand All @@ -15,13 +16,16 @@ abstract class NadelSchemaValidationFactory {
)

return NadelSchemaValidation(
fieldValidation = fieldValidation,
inputValidation = inputObjectValidation,
unionValidation = unionValidation,
enumValidation = enumValidation,
interfaceValidation = interfaceValidation,
namespaceValidation = namespaceValidation,
virtualTypeValidation = virtualTypeValidation,
typeValidation = NadelTypeValidation(
fieldValidation = fieldValidation,
inputObjectValidation = inputObjectValidation,
unionValidation = unionValidation,
enumValidation = enumValidation,
interfaceValidation = interfaceValidation,
namespaceValidation = namespaceValidation,
virtualTypeValidation = virtualTypeValidation,
defaultHydrationDefinitionValidation = defaultHydrationDefinitionValidation,
),
instructionDefinitionParser = definitionParser,
hook = hook,
)
Expand All @@ -43,6 +47,8 @@ abstract class NadelSchemaValidationFactory {
private val hydrationSourceFieldValidation = NadelHydrationSourceFieldValidation()
private val hydrationVirtualTypeValidation = NadelHydrationVirtualTypeValidation()

private val defaultHydrationDefinitionValidation = NadelDefaultHydrationDefinitionValidation()

private val hydrationValidation = NadelHydrationValidation(
argumentValidation = hydrationArgumentValidation,
conditionValidation = hydrationConditionValidation,
Expand Down
13 changes: 11 additions & 2 deletions lib/src/main/java/graphql/nadel/validation/NadelTypeValidation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import graphql.nadel.engine.util.operationTypes
import graphql.nadel.validation.NadelSchemaValidationError.DuplicatedUnderlyingType
import graphql.nadel.validation.NadelSchemaValidationError.IncompatibleType
import graphql.nadel.validation.NadelSchemaValidationError.MissingUnderlyingType
import graphql.nadel.validation.hydration.NadelDefaultHydrationDefinitionValidation
import graphql.nadel.validation.util.NadelBuiltInTypes.allNadelBuiltInTypeNames
import graphql.nadel.validation.util.NadelReferencedType
import graphql.nadel.validation.util.NadelSchemaUtil.getUnderlyingType
Expand All @@ -20,12 +21,13 @@ import graphql.schema.GraphQLUnionType

internal class NadelTypeValidation(
private val fieldValidation: NadelFieldValidation,
private val inputValidation: NadelInputObjectValidation,
private val inputObjectValidation: NadelInputObjectValidation,
private val unionValidation: NadelUnionValidation,
private val enumValidation: NadelEnumValidation,
private val interfaceValidation: NadelInterfaceValidation,
private val namespaceValidation: NadelNamespaceValidation,
private val virtualTypeValidation: NadelVirtualTypeValidation,
private val defaultHydrationDefinitionValidation: NadelDefaultHydrationDefinitionValidation,
) {
context(NadelValidationContext)
fun validate(
Expand Down Expand Up @@ -93,6 +95,12 @@ internal class NadelTypeValidation(
ok()
}

val defaultHydrationResult = if (schemaElement is NadelServiceSchemaElement.Type) {
defaultHydrationDefinitionValidation.validate(schemaElement)
} else {
ok()
}

val typeSpecificResult = when (schemaElement) {
is NadelServiceSchemaElement.Enum -> {
enumValidation.validate(schemaElement)
Expand All @@ -104,7 +112,7 @@ internal class NadelTypeValidation(
ok()
}
is NadelServiceSchemaElement.InputObject -> {
inputValidation.validate(schemaElement)
inputObjectValidation.validate(schemaElement)
}
is NadelServiceSchemaElement.Scalar -> {
ok()
Expand All @@ -123,6 +131,7 @@ internal class NadelTypeValidation(
return results(
fieldsContainerResult,
renameResult,
defaultHydrationResult,
typeSpecificResult,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package graphql.nadel.validation.hydration

import graphql.nadel.definition.hydration.NadelDefaultHydrationDefinition
import graphql.nadel.engine.util.getFieldAt
import graphql.nadel.engine.util.unwrapAll
import graphql.nadel.engine.util.whenType
import graphql.nadel.validation.NadelDefaultHydrationFieldNotFoundError
import graphql.nadel.validation.NadelDefaultHydrationIdArgumentNotFoundError
import graphql.nadel.validation.NadelDefaultHydrationIncompatibleBackingFieldTypeError
import graphql.nadel.validation.NadelSchemaValidationResult
import graphql.nadel.validation.NadelServiceSchemaElement
import graphql.nadel.validation.NadelValidationContext
import graphql.nadel.validation.ok

/**
* Validates a `@defaultHydration` before it's actually put to use.
*/
class NadelDefaultHydrationDefinitionValidation {
context(NadelValidationContext)
fun validate(type: NadelServiceSchemaElement.Type): NadelSchemaValidationResult {
val defaultHydration = instructionDefinitions.getDefaultHydrationOrNull(type)
?: return ok()

return validate(type, defaultHydration)
}

context(NadelValidationContext)
private fun validate(
type: NadelServiceSchemaElement.Type,
defaultHydration: NadelDefaultHydrationDefinition,
): NadelSchemaValidationResult {
val backingField = engineSchema.queryType.getFieldAt(defaultHydration.backingField)
?: return NadelDefaultHydrationFieldNotFoundError(type, defaultHydration)

val backingFieldType = backingField.type.unwrapAll()

// Backing field should return the type declaring the @defaultHydration
// It's ok if the backing field returns an abstract type and there are other possible options
// We have validation elsewhere that dictates that the hydrated field type must be valid
val backingFieldOutputTypeValid = backingFieldType.whenType(
enumType = { enumType -> enumType.name == type.overall.name },
inputObjectType = { inputObjectType -> inputObjectType.name == type.overall.name },
interfaceType = { interfaceType ->
interfaceType.name == type.overall.name
|| engineSchema.getImplementations(interfaceType).contains(type.overall)
},
objectType = { objectType -> objectType.name == type.overall.name },
scalarType = { scalarType -> scalarType.name == type.overall.name },
unionType = { unionType ->
unionType.name == type.overall.name
|| unionType.types.contains(type.overall)
},
)

if (!backingFieldOutputTypeValid) {
return NadelDefaultHydrationIncompatibleBackingFieldTypeError(type, defaultHydration, backingField)
}

if (backingField.getArgument(defaultHydration.idArgument) == null) {
return NadelDefaultHydrationIdArgumentNotFoundError(type, defaultHydration, backingField)
}

return ok()
}
}
Loading

0 comments on commit efee18f

Please sign in to comment.