Skip to content

Commit 8e848ab

Browse files
authored
BREAKING CHANGE: empty complex types should fail schema generation (ExpediaGroup#541)
* BREAKING CHANGE: empty complex types should fail schema generation GraphQL specification requires Query type to be present as it is required to run introspection query. Per specification it also shouldn't be possible to generate empty complex types (objects, input objects or interfaces) and they should expose at least a single field. Since root Query type is a special GraphQLObjectType it also has to expose at least a single field. Breaking changes: * at least single Query is required when generating the schema * split `didGenerateQueryType` hook (and corresponding `Mutation` and `Subscription` hooks) into `didGenerateQueryFieldType` (previous functionality) and `didGenerateQueryObjectType` hooks to allow more granular control when generating the schema * default `SchemaGeneratorHooks` now performs validation of generated object types (including special query, mutation and subscription types), input object types and interfaces to ensure we don't generate empty complex object types see: graphql/graphql-spec#490 and graphql/graphql-spec#568
1 parent b1423ce commit 8e848ab

28 files changed

+413
-78
lines changed

detekt.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ complexity:
1313
threshold: 10
1414
active: true
1515
TooManyFunctions:
16-
thresholdInInterfaces: 15
16+
thresholdInInterfaces: 20
1717
thresholdInClasses: 15
1818
thresholdInFiles: 15
1919
ComplexInterface:
20-
threshold: 15
20+
threshold: 20
2121

2222
naming:
2323
FunctionMaxLength:

graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorHooks.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: Fede
108108
return federatedSchema.query(federatedQuery.build())
109109
.codeRegistry(federatedCodeRegistry.build())
110110
}
111+
112+
// skip validation for empty query type - federation will add _service query
113+
override fun didGenerateQueryObject(type: GraphQLObjectType): GraphQLObjectType = type
111114
}
112115

113116
private fun TypeResolutionEnvironment.getObjectName(): String? {

graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/toFederatedSchema.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -108,7 +108,7 @@ class FederatedSchemaGeneratorTest {
108108
hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry())
109109
)
110110

111-
val schema = toFederatedSchema(config)
111+
val schema = toFederatedSchema(config = config)
112112
assertEquals(FEDERATED_SDL, schema.print().trim())
113113
val productType = schema.getObjectType("Book")
114114
assertNotNull(productType)

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/data/TestSchema.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,18 +16,21 @@
1616

1717
package com.expediagroup.graphql.federation.data
1818

19+
import com.expediagroup.graphql.TopLevelObject
1920
import com.expediagroup.graphql.federation.FederatedSchemaGeneratorConfig
2021
import com.expediagroup.graphql.federation.FederatedSchemaGeneratorHooks
2122
import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry
2223
import com.expediagroup.graphql.federation.execution.FederatedTypeResolver
2324
import com.expediagroup.graphql.federation.toFederatedSchema
2425
import graphql.schema.GraphQLSchema
2526

26-
internal fun federatedTestSchema(federatedTypeResolvers: Map<String, FederatedTypeResolver<*>> = emptyMap()): GraphQLSchema {
27+
internal fun federatedTestSchema(
28+
queries: List<TopLevelObject> = emptyList(),
29+
federatedTypeResolvers: Map<String, FederatedTypeResolver<*>> = emptyMap()
30+
): GraphQLSchema {
2731
val config = FederatedSchemaGeneratorConfig(
2832
supportedPackages = listOf("com.expediagroup.graphql.federation.data.queries.federated"),
2933
hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry(federatedTypeResolvers))
3034
)
31-
32-
return toFederatedSchema(config)
35+
return toFederatedSchema(config = config, queries = queries)
3336
}

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/FederatedQueryResolverTest.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,12 +16,12 @@
1616

1717
package com.expediagroup.graphql.federation.execution
1818

19-
import graphql.ExecutionInput
20-
import graphql.GraphQL
21-
import org.junit.jupiter.api.Test
2219
import com.expediagroup.graphql.federation.data.BookResolver
2320
import com.expediagroup.graphql.federation.data.UserResolver
2421
import com.expediagroup.graphql.federation.data.federatedTestSchema
22+
import graphql.ExecutionInput
23+
import graphql.GraphQL
24+
import org.junit.jupiter.api.Test
2525
import kotlin.test.assertEquals
2626
import kotlin.test.assertFalse
2727
import kotlin.test.assertNotNull
@@ -47,7 +47,9 @@ class FederatedQueryResolverTest {
4747

4848
@Test
4949
fun `verify can resolve federated entities`() {
50-
val schema = federatedTestSchema(mapOf("Book" to BookResolver(), "User" to UserResolver()))
50+
val schema = federatedTestSchema(
51+
federatedTypeResolvers = mapOf("Book" to BookResolver(), "User" to UserResolver())
52+
)
5153
val userRepresentation = mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName")
5254
val book1Representation = mapOf<String, Any>("__typename" to "Book", "id" to 987, "weight" to 2.0)
5355
val book2Representation = mapOf<String, Any>("__typename" to "Book", "id" to 988, "weight" to 1.0)

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/ServiceQueryResolverTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,12 +19,12 @@ package com.expediagroup.graphql.federation.execution
1919
import com.expediagroup.graphql.TopLevelObject
2020
import com.expediagroup.graphql.federation.FederatedSchemaGeneratorConfig
2121
import com.expediagroup.graphql.federation.FederatedSchemaGeneratorHooks
22+
import com.expediagroup.graphql.federation.data.queries.simple.NestedQuery
23+
import com.expediagroup.graphql.federation.data.queries.simple.SimpleQuery
2224
import com.expediagroup.graphql.federation.toFederatedSchema
2325
import graphql.ExecutionInput
2426
import graphql.GraphQL
2527
import org.junit.jupiter.api.Test
26-
import com.expediagroup.graphql.federation.data.queries.simple.NestedQuery
27-
import com.expediagroup.graphql.federation.data.queries.simple.SimpleQuery
2828
import kotlin.test.assertEquals
2929
import kotlin.test.assertNotNull
3030

@@ -76,7 +76,7 @@ class ServiceQueryResolverTest {
7676
hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry())
7777
)
7878

79-
val schema = toFederatedSchema(config)
79+
val schema = toFederatedSchema(config = config)
8080
val query = """
8181
query sdlQuery {
8282
_service {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.exceptions
18+
19+
import com.expediagroup.graphql.generator.extensions.getSimpleName
20+
import kotlin.reflect.KType
21+
22+
/**
23+
* Thrown when schema input object type does not expose any fields. Since GraphQL always requires you to explicitly specify all the fields down to scalar values, using an input object type without
24+
* any defined fields would result in a field that is impossible to query without producing an error.
25+
*
26+
* @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568)
27+
*/
28+
class EmptyInputObjectTypeException(ktype: KType) : GraphQLKotlinException("Invalid ${ktype.getSimpleName(isInputType = true)} input object type - input object does not expose any fields.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.exceptions
18+
19+
import com.expediagroup.graphql.generator.extensions.getSimpleName
20+
import kotlin.reflect.KType
21+
22+
/**
23+
* Thrown when interface type does not expose any fields - this should never happen unless we explicitly exclude fields from interface either through
24+
* annotating fields with @GraphQLIgnore or by custom hooks that filter out available functions. Since GraphQL always requires you to select fields down
25+
* to scalar values, an object type without any defined fields cannot be accessed in any way in a query.
26+
*
27+
* @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568)
28+
*/
29+
class EmptyInterfaceTypeException(ktype: KType) : GraphQLKotlinException("Invalid ${ktype.getSimpleName()} interface type - interface does not expose any fields.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.exceptions
18+
19+
/**
20+
* Thrown when generated GraphQL Mutation type does not expose any fields.
21+
*
22+
* @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568)
23+
*/
24+
object EmptyMutationTypeException : GraphQLKotlinException("Invalid mutation object type - no valid mutations are available.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.exceptions
18+
19+
import com.expediagroup.graphql.generator.extensions.getSimpleName
20+
import kotlin.reflect.KType
21+
22+
/**
23+
* Thrown when schema object type does not expose any fields. Since GraphQL always requires you to select fields down to scalar values, an object type without any defined fields cannot be accessed
24+
* in any way in a query.
25+
*
26+
* @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568)
27+
*/
28+
class EmptyObjectTypeException(ktype: KType) : GraphQLKotlinException("Invalid ${ktype.getSimpleName()} object type - object does not expose any fields.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.exceptions
18+
19+
/**
20+
* Thrown when generated GraphQL Query type does not expose any fields.
21+
*
22+
* @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568)
23+
*/
24+
object EmptyQueryTypeException : GraphQLKotlinException("Invalid query object type - no valid queries are available.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2020 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.exceptions
18+
19+
/**
20+
* Thrown when generated GraphQL Subscription type does not expose any fields.
21+
*
22+
* @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568)
23+
*/
24+
object EmptySubscriptionTypeException : GraphQLKotlinException("Invalid subscription object type - no valid subscriptions are available.")

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/InvalidInterfaceException.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,12 @@
1616

1717
package com.expediagroup.graphql.exceptions
1818

19+
import kotlin.reflect.KClass
20+
1921
/**
20-
* Thrown when a interface implements another interface or abstract class that is not exluded from the schema.
22+
* Thrown when an interface implements another interface or abstract class that is not excluded from the schema.
2123
*
2224
* This is an invalid schema until the GraphQL spec is updated
2325
* https://github.com/ExpediaGroup/graphql-kotlin/issues/419
2426
*/
25-
class InvalidInterfaceException : GraphQLKotlinException("Interfaces can not have any superclasses")
27+
class InvalidInterfaceException(klazz: KClass<*>) : GraphQLKotlinException("Invalid ${klazz.simpleName} interface - interfaces can not have any superclasses.")

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/InterfaceBuilder.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,7 +34,7 @@ import kotlin.reflect.full.createType
3434
internal fun generateInterface(generator: SchemaGenerator, kClass: KClass<*>): GraphQLInterfaceType {
3535
// Interfaces can not implement another interface in GraphQL
3636
if (kClass.getValidSuperclasses(generator.config.hooks).isNotEmpty()) {
37-
throw InvalidInterfaceException()
37+
throw InvalidInterfaceException(klazz = kClass)
3838
}
3939

4040
val builder = GraphQLInterfaceType.newInterface()

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/MutationBuilder.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,7 +24,6 @@ import com.expediagroup.graphql.generator.extensions.isNotPublic
2424
import graphql.schema.GraphQLObjectType
2525

2626
fun generateMutations(generator: SchemaGenerator, mutations: List<TopLevelObject>): GraphQLObjectType? {
27-
2827
if (mutations.isEmpty()) {
2928
return null
3029
}
@@ -44,10 +43,10 @@ fun generateMutations(generator: SchemaGenerator, mutations: List<TopLevelObject
4443
mutation.kClass.getValidFunctions(generator.config.hooks)
4544
.forEach {
4645
val function = generateFunction(generator, it, generator.config.topLevelNames.mutation, mutation.obj)
47-
val functionFromHook = generator.config.hooks.didGenerateMutationType(mutation.kClass, it, function)
46+
val functionFromHook = generator.config.hooks.didGenerateMutationField(mutation.kClass, it, function)
4847
mutationBuilder.field(functionFromHook)
4948
}
5049
}
5150

52-
return mutationBuilder.build()
51+
return generator.config.hooks.didGenerateMutationObject(mutationBuilder.build())
5352
}

graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/QueryBuilder.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2020 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,10 +39,10 @@ fun generateQueries(generator: SchemaGenerator, queries: List<TopLevelObject>):
3939
query.kClass.getValidFunctions(generator.config.hooks)
4040
.forEach {
4141
val function = generateFunction(generator, it, generator.config.topLevelNames.query, query.obj)
42-
val functionFromHook = generator.config.hooks.didGenerateQueryType(query.kClass, it, function)
42+
val functionFromHook = generator.config.hooks.didGenerateQueryField(query.kClass, it, function)
4343
queryBuilder.field(functionFromHook)
4444
}
4545
}
4646

47-
return queryBuilder.build()
47+
return generator.config.hooks.didGenerateQueryObject(queryBuilder.build())
4848
}

0 commit comments

Comments
 (0)