Skip to content

Commit

Permalink
Add validation for virtual types
Browse files Browse the repository at this point in the history
  • Loading branch information
gnawf committed Nov 18, 2024
1 parent a8359f2 commit 57b5fce
Show file tree
Hide file tree
Showing 19 changed files with 861 additions and 321 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class NadelHydrationDefinition(
)
}

val backingField: List<String>
val pathToBackingField: List<String>
get() = appliedDirective.getArgument(Keyword.field).getValue<String>().split(".")

val identifiedBy: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ private class Factory(
virtualFieldDef: GraphQLFieldDefinition,
hydration: NadelHydrationDefinition,
): NadelFieldInstruction {
val pathToBackingField = hydration.backingField
val pathToBackingField = hydration.pathToBackingField
val backingFieldContainer = engineSchema.queryType.getFieldContainerFor(pathToBackingField)!!
val backingFieldDef = engineSchema.queryType.getFieldAt(pathToBackingField)!!
val hydrationBackingService = coordinatesToService[makeFieldCoordinates(backingFieldContainer, backingFieldDef)]!!
Expand Down Expand Up @@ -412,7 +412,7 @@ private class Factory(
location = location,
virtualFieldDef = virtualFieldDef,
backingService = backingService,
queryPathToBackingField = NadelQueryPath(hydration.backingField),
queryPathToBackingField = NadelQueryPath(hydration.pathToBackingField),
backingFieldArguments = hydrationArgs,
timeout = hydration.timeout,
batchSize = batchSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal class NadelVirtualTypeBlueprintFactory {
virtualFieldDef: GraphQLFieldDefinition,
): List<VirtualTypeMapping> {
val hydration = virtualFieldDef.getHydrationDefinitions().first()
val backingFieldDef = engineSchema.queryType.getFieldAt(hydration.backingField)!!
val backingFieldDef = engineSchema.queryType.getFieldAt(hydration.pathToBackingField)!!
val backingType = backingFieldDef.type.unwrapAll() as? GraphQLObjectType
?: return emptyList()
val virtualType = virtualFieldDef.type.unwrapAll() as? GraphQLObjectType
Expand Down
59 changes: 31 additions & 28 deletions lib/src/main/java/graphql/nadel/validation/NadelFieldValidation.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
package graphql.nadel.validation

import graphql.nadel.definition.hydration.isHydrated
import graphql.nadel.definition.renamed.getRenamedOrNull
import graphql.nadel.definition.renamed.isRenamed
import graphql.nadel.engine.util.strictAssociateBy
import graphql.nadel.engine.util.unwrapAll
import graphql.nadel.validation.NadelSchemaValidationError.IncompatibleArgumentInputType
import graphql.nadel.validation.NadelSchemaValidationError.IncompatibleFieldOutputType
import graphql.nadel.validation.NadelSchemaValidationError.MissingArgumentOnUnderlying
import graphql.nadel.validation.NadelSchemaValidationError.MissingUnderlyingField
import graphql.nadel.validation.NadelTypeWrappingValidation.Rule.LHS_MUST_BE_LOOSER_OR_SAME
import graphql.nadel.validation.hydration.NadelHydrationValidation
import graphql.nadel.validation.util.NadelCombinedTypeUtil.getFieldsThatServiceContributed
import graphql.nadel.validation.util.NadelSchemaUtil.getUnderlyingName
import graphql.schema.GraphQLArgument
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLNamedSchemaElement
import graphql.schema.GraphQLOutputType

internal class NadelFieldValidation(
private val typeValidation: NadelTypeValidation,
private val hydrationValidation: NadelHydrationValidation,
) {
private val renameValidation = NadelRenameValidation(this)
private val inputValidation = NadelInputValidation()
private val hydrationValidation = NadelHydrationValidation(typeValidation)
private val partitionValidation = NadelPartitionValidation()
private val typeWrappingValidation = NadelTypeWrappingValidation()

Expand Down Expand Up @@ -96,24 +98,22 @@ internal class NadelFieldValidation(
if (underlyingArg == null) {
MissingArgumentOnUnderlying(parent, overallField, underlyingField, overallArg)
} else {
val unwrappedTypeIssues = typeValidation
.validate(
NadelServiceSchemaElement.from(
service = parent.service,
overall = overallArg.type.unwrapAll(),
underlying = underlyingArg.type.unwrapAll(),
if (isUnwrappedArgTypeSame(overallArg, underlyingArg)) {
inputValidation
.validate(
parent = parent,
overallField = overallField,
overallInputArgument = overallArg,
underlyingInputArgument = underlyingArg
)
)

val inputTypeIssues = inputValidation
.validate(
parent = parent,
} else {
IncompatibleArgumentInputType(
parentType = parent,
overallField = overallField,
overallInputArgument = overallArg,
underlyingInputArgument = underlyingArg
overallInputArg = overallArg,
underlyingInputArg = underlyingArg,
)

results(unwrappedTypeIssues, inputTypeIssues)
}
}
}
.toResult()
Expand All @@ -124,6 +124,16 @@ internal class NadelFieldValidation(
return results(argumentIssues, outputTypeIssues, partitionDirectiveIssues)
}

private fun isUnwrappedArgTypeSame(
overallArg: GraphQLArgument,
underlyingArg: GraphQLArgument,
): Boolean {
val overallArgTypeUnwrapped = overallArg.type.unwrapAll()
val underlyingArgTypeUnwrapped = underlyingArg.type.unwrapAll()
val expectedUnderlyingName = overallArgTypeUnwrapped.getRenamedOrNull()?.from ?: overallArgTypeUnwrapped.name
return expectedUnderlyingName == underlyingArgTypeUnwrapped.name
}

context(NadelValidationContext)
private fun validateOutputType(
parent: NadelServiceSchemaElement.FieldsContainer,
Expand All @@ -133,22 +143,15 @@ internal class NadelFieldValidation(
val overallType = overallField.type.unwrapAll()
val underlyingType = underlyingField.type.unwrapAll()

val typeServiceSchemaElement = NadelServiceSchemaElement.from(
service = parent.service,
overall = overallType,
underlying = underlyingType,
)

// This checks whether the type is actually valid content wise
val outputTypeResult = typeValidation.validate(typeServiceSchemaElement)
.onError { return it }
if ((overallType.getRenamedOrNull()?.from ?: overallType.name) != underlyingType.name) {
return IncompatibleFieldOutputType(parent, overallField, underlyingField)
}

// This checks whether the output type e.g. name or List or NonNull wrappings are valid
return if (isOutputTypeValid(overallType = overallField.type, underlyingType = underlyingField.type)) {
outputTypeResult
ok()
} else {
results(
outputTypeResult,
IncompatibleFieldOutputType(parent, overallField, underlyingField),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private fun getHydrationErrorMessage(
): String {
val parentTypeName = parentType.overall.name
val fieldName = virtualField.name
val backingField = hydration.backingField.joinToString(separator = ".")
val backingField = hydration.pathToBackingField.joinToString(separator = ".")
return "Field $parentTypeName.$fieldName tried to hydrate from Query.$backingField but $reason"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import graphql.ErrorClassification
import graphql.GraphQLError
import graphql.GraphqlErrorBuilder
import graphql.nadel.Service
import graphql.nadel.definition.hydration.NadelBatchObjectIdentifiedByDefinition
import graphql.nadel.definition.hydration.NadelHydrationArgumentDefinition
import graphql.nadel.definition.hydration.NadelHydrationDefinition
import graphql.nadel.definition.renamed.NadelRenamedDefinition
Expand Down Expand Up @@ -237,7 +236,7 @@ sealed interface NadelSchemaValidationError : NadelSchemaValidationResult {

override val message = run {
val vf = makeFieldCoordinates(parentType.overall.name, overallField.name)
val bf = hydration.backingField.joinToString(separator = ".")
val bf = hydration.pathToBackingField.joinToString(separator = ".")
val an = hydrationArgument.name
"Field $vf tried to hydrate Query.$bf but gave invalid static value for argument $an"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal sealed interface NadelValidationInterimResult<T> {
) : NadelValidationInterimResult<T> {
companion object {
context(NadelValidationContext)
internal fun <T : Any> T.asInterimSuccess(): Success<T> {
internal fun <T : Any?> T.asInterimSuccess(): Success<T> {
return Success(this)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ sealed class NadelServiceSchemaElement {
override val underlying: GraphQLNamedSchemaElement,
) : NadelServiceSchemaElement()

data class VirtualType(
override val service: Service,
override val overall: GraphQLNamedType,
/**
* In the case of virtual types, this is the backing type.
*/
override val underlying: GraphQLNamedType,
) : Type()

companion object {
fun from(
service: Service,
Expand Down
98 changes: 41 additions & 57 deletions lib/src/main/java/graphql/nadel/validation/NadelTypeValidation.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
package graphql.nadel.validation

import graphql.Scalars.GraphQLID
import graphql.Scalars.GraphQLString
import graphql.language.UnionTypeDefinition
import graphql.nadel.Service
import graphql.nadel.definition.virtualType.isVirtualType
import graphql.nadel.engine.blueprint.NadelTypeRenameInstruction
import graphql.nadel.engine.util.AnyNamedNode
import graphql.nadel.engine.util.isExtensionDef
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.NadelHydrationValidation
import graphql.nadel.validation.util.NadelBuiltInTypes.allNadelBuiltInTypeNames
import graphql.nadel.validation.util.NadelReferencedType
import graphql.nadel.validation.util.NadelSchemaUtil.getUnderlyingType
import graphql.nadel.validation.util.getReachableTypeNames
import graphql.schema.GraphQLDirectiveContainer
import graphql.schema.GraphQLImplementingType
import graphql.schema.GraphQLInterfaceType
import graphql.schema.GraphQLNamedOutputType
import graphql.nadel.validation.util.getReferencedTypeNames
import graphql.schema.GraphQLNamedType
import graphql.schema.GraphQLObjectType
import graphql.schema.GraphQLUnionType

internal class NadelTypeValidation {
private val fieldValidation = NadelFieldValidation(this)
private val hydrationValidation = NadelHydrationValidation()
private val fieldValidation = NadelFieldValidation(hydrationValidation)
private val inputValidation = NadelInputValidation()
private val unionValidation = NadelUnionValidation(this)
private val enumValidation = NadelEnumValidation()
private val interfaceValidation = NadelInterfaceValidation()
private val namespaceValidation = NadelNamespaceValidation()
private val virtualTypeValidation = NadelVirtualTypeValidation(hydrationValidation)

context(NadelValidationContext)
fun validate(
Expand Down Expand Up @@ -115,6 +112,9 @@ internal class NadelTypeValidation {
is NadelServiceSchemaElement.Incompatible -> {
IncompatibleType(schemaElement)
}
is NadelServiceSchemaElement.VirtualType -> {
virtualTypeValidation.validate(schemaElement)
}
}

return results(
Expand Down Expand Up @@ -146,67 +146,48 @@ internal class NadelTypeValidation {
)
}

/**
* Answers whether `rhs` assignable to `lhs`?
*
* i.e. does the following compile
*
* ```
* vol output: lhs = rhs
* ```
*
* Note: this assumes both types are from the same schema. This does NOT
* deal with differences between overall and underlying schema.
*/
context(NadelValidationContext)
fun isAssignableTo(lhs: GraphQLNamedOutputType, rhs: GraphQLNamedOutputType): Boolean {
if (lhs.name == rhs.name) {
return true
}
if (lhs.name == GraphQLID.name && rhs.name == GraphQLString.name) {
return true
}
if (lhs is GraphQLInterfaceType && rhs is GraphQLImplementingType) {
return rhs.interfaces.contains(lhs)
}
return false
}

context(NadelValidationContext)
private fun getServiceTypes(
service: Service,
): Pair<List<NadelServiceSchemaElement>, NadelSchemaValidationResult> {
val errors = mutableListOf<NadelSchemaValidationError>()
val hydrationUnions = getHydrationUnions(service)
val namesUsed = getTypeNamesUsed(service, externalTypes = hydrationUnions)
val referencedTypes = getReferencedTypes(service, externalTypes = hydrationUnions)

fun addMissingUnderlyingTypeError(overallType: GraphQLNamedType) {
errors.add(MissingUnderlyingType(service, overallType))
}

return namesUsed
.map {
engineSchema.typeMap[it]!!
}
return referencedTypes
.filterNot {
it.name in allNadelBuiltInTypeNames
}
.mapNotNull { overallType ->
val underlyingType = getUnderlyingType(overallType, service)

if (underlyingType == null) {
if ((overallType as? GraphQLDirectiveContainer)?.isVirtualType() == true) {
// Do nothing
} else {
addMissingUnderlyingTypeError(overallType)
.mapNotNull { referencedType ->
when (referencedType) {
is NadelReferencedType.OrdinaryType -> {
val overallType = engineSchema.typeMap[referencedType.name]!!
val underlyingType = getUnderlyingType(overallType, service)

if (underlyingType == null) {
addMissingUnderlyingTypeError(overallType)
null
} else {
NadelServiceSchemaElement.from(
service = service,
overall = overallType,
underlying = underlyingType,
)
}
}
is NadelReferencedType.VirtualType -> {
val virtualType = engineSchema.typeMap[referencedType.name]!!
val backingType = engineSchema.typeMap[referencedType.backingType]!!
NadelServiceSchemaElement.VirtualType(
service = service,
overall = virtualType,
underlying = backingType,
)
}
null
} else {
NadelServiceSchemaElement.from(
service = service,
overall = overallType,
underlying = underlyingType,
)
}
}
.toList()
Expand Down Expand Up @@ -239,7 +220,10 @@ internal class NadelTypeValidation {
}

context(NadelValidationContext)
private fun getTypeNamesUsed(service: Service, externalTypes: List<GraphQLNamedType>): Set<String> {
private fun getReferencedTypes(
service: Service,
externalTypes: List<GraphQLNamedType>,
): Set<NadelReferencedType> {
// There is no shared service to validate.
// These shared types are USED in other services. When they are used, the validation
// will validate that the service has a compatible underlying type.
Expand Down Expand Up @@ -281,7 +265,7 @@ internal class NadelTypeValidation {
}

// If it can be reached by using your service, you must own it to return it!
return getReachableTypeNames(service, definitionNames + matchingImplementsNames)
return getReferencedTypeNames(service, definitionNames + matchingImplementsNames)
}

context(NadelValidationContext)
Expand Down
Loading

0 comments on commit 57b5fce

Please sign in to comment.