Skip to content

Commit

Permalink
fix(federation): relaxes @requires and @external constraints (#1778)
Browse files Browse the repository at this point in the history
Relaxing `@requires` and `@external` constraints. Previously ALL fields referenced from the `@requires` selection set had to be marked `@external`. This was too strict as it is possible to define `@requires` selection set against **local** object type with some `@external` fields. As a result only leaf fields (i.e. scalar/enum fields) have to be explicitly marked `@external` OR implicitly inherit `@external` characteristic through any of the leaf field ancestors.
  • Loading branch information
dariuszkuc authored May 23, 2023
1 parent 98e900e commit 5cad531
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import graphql.schema.GraphQLType
import graphql.schema.GraphQLTypeUtil
import graphql.schema.GraphQLUnionType

internal fun validateFieldSelection(validatedDirective: DirectiveInfo, selection: FieldSetSelection, targetType: GraphQLType, errors: MutableList<String>) {
internal fun validateFieldSelection(validatedDirective: DirectiveInfo, selection: FieldSetSelection, targetType: GraphQLType, errors: MutableList<String>, isExternalPath: Boolean = false) {
when (val unwrapped = GraphQLTypeUtil.unwrapNonNull(targetType)) {
is GraphQLScalarType, is GraphQLEnumType -> {
if (selection.subSelections.isNotEmpty()) {
Expand All @@ -38,7 +38,7 @@ internal fun validateFieldSelection(validatedDirective: DirectiveInfo, selection
if (KEY_DIRECTIVE_NAME == validatedDirective.directiveName) {
errors.add("$validatedDirective specifies invalid field set - field set references GraphQLList, field=${selection.field}")
} else {
validateFieldSelection(validatedDirective, selection, GraphQLTypeUtil.unwrapOne(targetType), errors)
validateFieldSelection(validatedDirective, selection, GraphQLTypeUtil.unwrapOne(targetType), errors, isExternalPath)
}
}
is GraphQLInterfaceType -> {
Expand All @@ -51,7 +51,8 @@ internal fun validateFieldSelection(validatedDirective: DirectiveInfo, selection
validatedDirective,
selection.subSelections,
unwrapped.fieldDefinitions.associateBy { it.name },
errors
errors,
isExternalPath
)
}
}
Expand All @@ -63,7 +64,8 @@ internal fun validateFieldSelection(validatedDirective: DirectiveInfo, selection
validatedDirective,
selection.subSelections,
unwrapped.fieldDefinitions.associateBy { it.name },
errors
errors,
isExternalPath
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,7 +26,7 @@ internal fun validateDirective(
validatedType: String,
targetDirective: String,
directiveMap: Map<String, List<GraphQLAppliedDirective>>,
fieldMap: Map<String, GraphQLFieldDefinition>,
fieldMap: Map<String, GraphQLFieldDefinition>
): List<String> {
val validationErrors = mutableListOf<String>()
val directives = directiveMap[targetDirective]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,24 +18,32 @@ package com.expediagroup.graphql.generator.federation.validation

import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
import graphql.schema.GraphQLDirectiveContainer
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLNamedType
import graphql.schema.GraphQLTypeUtil

internal fun validateFieldSetSelection(
validatedDirective: DirectiveInfo,
selections: List<FieldSetSelection>,
fields: Map<String, GraphQLFieldDefinition>,
errors: MutableList<String>
errors: MutableList<String>,
isExternalPath: Boolean = false
) {
for (selection in selections) {
val currentField = fields[selection.field]
if (currentField == null) {
errors.add("$validatedDirective specifies invalid field set - field set specifies field that does not exist, field=${selection.field}")
} else {
val currentFieldType = currentField.type
if (REQUIRES_DIRECTIVE_NAME == validatedDirective.directiveName && currentField.getAppliedDirective(EXTERNAL_DIRECTIVE_NAME) == null) {
val isExternal = isExternalPath || GraphQLTypeUtil.unwrapAll(currentFieldType).isExternalPath() || currentField.isExternalType()
if (REQUIRES_DIRECTIVE_NAME == validatedDirective.directiveName && GraphQLTypeUtil.isLeaf(currentFieldType) && !isExternal) {
errors.add("$validatedDirective specifies invalid field set - @requires should reference only @external fields, field=${selection.field}")
}
validateFieldSelection(validatedDirective, selection, currentFieldType, errors)
validateFieldSelection(validatedDirective, selection, currentFieldType, errors, isExternal)
}
}
}

private fun GraphQLDirectiveContainer.isExternalType(): Boolean = this.getAppliedDirectives(EXTERNAL_DIRECTIVE_NAME).isNotEmpty()
private fun GraphQLNamedType.isExternalPath(): Boolean = this is GraphQLDirectiveContainer && this.isExternalType()
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.requires.success._5

import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
import com.expediagroup.graphql.generator.federation.directives.RequiresDirective
import kotlin.properties.Delegates

/*
# only leaf fields have to be external when @requires references complex types
type LeafRequires @key(fields : "id") {
complexType: ComplexType!
description: String!
id: String!
shippingCost: String! @requires(fields : "complexType { weight }")
}
type ComplexType {
localField: String
weight: Float! @external
}
*/
@KeyDirective(fields = FieldSet("id"))
class LeafRequires(val id: String, val description: String, val complexType: ComplexType) {

@RequiresDirective(FieldSet("complexType { weight }"))
fun shippingCost(): String = "$${complexType.weight * 9.99}"
}

class ComplexType(val localField: String) {
@ExternalDirective
var weight: Double by Delegates.notNull()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.requires.success._6

import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
import com.expediagroup.graphql.generator.federation.directives.RequiresDirective
import kotlin.properties.Delegates

/*
# @external information is applied recursively through parent fields
type RecursiveExternalRequires @key(fields : "id") {
complexType: ComplexType! @external
description: String!
id: String!
shippingCost: String! @requires(fields : "complexType { weight }")
}
type ComplexType {
potentiallyExternal: String
weight: Float!
}
*/
@KeyDirective(fields = FieldSet("id"))
class RecursiveExternalRequires(val id: String, val description: String, @ExternalDirective val complexType: ComplexType) {

@RequiresDirective(FieldSet("complexType { weight }"))
fun shippingCost(): String = "$${complexType.weight * 9.99}"
}

class ComplexType(val potentiallyExternal: String) {
var weight: Double by Delegates.notNull()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.data.integration.requires.success._7

import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
import com.expediagroup.graphql.generator.federation.directives.FieldSet
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
import com.expediagroup.graphql.generator.federation.directives.RequiresDirective
import kotlin.properties.Delegates

/*
# @external information is applied to fields within type
type RecursiveExternalRequires @key(fields : "id") {
externalType: ExternalType!
description: String!
id: String!
shippingCost: String! @requires(fields : "complexType { weight }")
}
type ExternalType @external {
allExternal: String
weight: Float!
}
*/
@KeyDirective(fields = FieldSet("id"))
class ExternalRequiresType(val id: String, val description: String, val externalType: ExternalType) {

@RequiresDirective(FieldSet("externalType { weight }"))
fun shippingCost(): String = "$${externalType.weight * 9.99}"
}

@ExternalDirective
class ExternalType(val allExternal: String) {
var weight: Double by Delegates.notNull()
}
Loading

0 comments on commit 5cad531

Please sign in to comment.