From 8e848ab3602ccd46ea8a7eb2de88c92a351074ac Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Fri, 10 Jan 2020 12:53:17 -0600 Subject: [PATCH] BREAKING CHANGE: empty complex types should fail schema generation (#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: https://github.com/graphql/graphql-spec/issues/490 and https://github.com/graphql/graphql-spec/issues/568 --- detekt.yml | 4 +- .../FederatedSchemaGeneratorHooks.kt | 3 + .../graphql/federation/toFederatedSchema.kt | 2 +- .../FederatedSchemaGeneratorTest.kt | 4 +- .../graphql/federation/data/TestSchema.kt | 11 ++- .../execution/FederatedQueryResolverTest.kt | 12 +-- .../execution/ServiceQueryResolverTest.kt | 8 +- .../EmptyInputObjectTypeException.kt | 28 ++++++ .../exceptions/EmptyInterfaceTypeException.kt | 29 +++++++ .../exceptions/EmptyMutationTypeException.kt | 24 ++++++ .../exceptions/EmptyObjectTypeException.kt | 28 ++++++ .../exceptions/EmptyQueryTypeException.kt | 24 ++++++ .../EmptySubscriptionTypeException.kt | 24 ++++++ .../exceptions/InvalidInterfaceException.kt | 8 +- .../generator/types/InterfaceBuilder.kt | 4 +- .../generator/types/MutationBuilder.kt | 7 +- .../graphql/generator/types/QueryBuilder.kt | 6 +- .../generator/types/SubscriptionBuilder.kt | 7 +- .../graphql/hooks/SchemaGeneratorHooks.kt | 62 +++++++++++-- .../graphql/generator/PolymorphicTests.kt | 7 +- .../generator/types/MutationBuilderTest.kt | 14 +-- .../generator/types/QueryBuilderTest.kt | 21 +++-- .../types/SubscriptionBuilderTest.kt | 15 +++- .../generator/types/UnionBuilderTest.kt | 12 ++- .../graphql/hooks/SchemaGeneratorHooksTest.kt | 86 ++++++++++++++++--- .../spring/SubscriptionConfigurationTest.kt | 16 +++- .../execution/SubscriptionHandlerTest.kt | 11 ++- .../SubscriptionWebSocketHandlerIT.kt | 14 ++- 28 files changed, 413 insertions(+), 78 deletions(-) create mode 100644 graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInputObjectTypeException.kt create mode 100644 graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInterfaceTypeException.kt create mode 100644 graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyMutationTypeException.kt create mode 100644 graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyObjectTypeException.kt create mode 100644 graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyQueryTypeException.kt create mode 100644 graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptySubscriptionTypeException.kt diff --git a/detekt.yml b/detekt.yml index c213011e91..b7ea77d8e4 100644 --- a/detekt.yml +++ b/detekt.yml @@ -13,11 +13,11 @@ complexity: threshold: 10 active: true TooManyFunctions: - thresholdInInterfaces: 15 + thresholdInInterfaces: 20 thresholdInClasses: 15 thresholdInFiles: 15 ComplexInterface: - threshold: 15 + threshold: 20 naming: FunctionMaxLength: diff --git a/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorHooks.kt b/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorHooks.kt index a3a0e1be08..d54b41ce29 100644 --- a/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorHooks.kt +++ b/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorHooks.kt @@ -108,6 +108,9 @@ open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: Fede return federatedSchema.query(federatedQuery.build()) .codeRegistry(federatedCodeRegistry.build()) } + + // skip validation for empty query type - federation will add _service query + override fun didGenerateQueryObject(type: GraphQLObjectType): GraphQLObjectType = type } private fun TypeResolutionEnvironment.getObjectName(): String? { diff --git a/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/toFederatedSchema.kt b/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/toFederatedSchema.kt index 28bc997d05..81e827cdc6 100644 --- a/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/toFederatedSchema.kt +++ b/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/toFederatedSchema.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorTest.kt b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorTest.kt index cbbff12cff..67d9f65000 100644 --- a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorTest.kt +++ b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,7 +108,7 @@ class FederatedSchemaGeneratorTest { hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry()) ) - val schema = toFederatedSchema(config) + val schema = toFederatedSchema(config = config) assertEquals(FEDERATED_SDL, schema.print().trim()) val productType = schema.getObjectType("Book") assertNotNull(productType) diff --git a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/data/TestSchema.kt b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/data/TestSchema.kt index e7c19159d8..88c186289b 100644 --- a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/data/TestSchema.kt +++ b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/data/TestSchema.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.expediagroup.graphql.federation.data +import com.expediagroup.graphql.TopLevelObject import com.expediagroup.graphql.federation.FederatedSchemaGeneratorConfig import com.expediagroup.graphql.federation.FederatedSchemaGeneratorHooks import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry @@ -23,11 +24,13 @@ import com.expediagroup.graphql.federation.execution.FederatedTypeResolver import com.expediagroup.graphql.federation.toFederatedSchema import graphql.schema.GraphQLSchema -internal fun federatedTestSchema(federatedTypeResolvers: Map> = emptyMap()): GraphQLSchema { +internal fun federatedTestSchema( + queries: List = emptyList(), + federatedTypeResolvers: Map> = emptyMap() +): GraphQLSchema { val config = FederatedSchemaGeneratorConfig( supportedPackages = listOf("com.expediagroup.graphql.federation.data.queries.federated"), hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry(federatedTypeResolvers)) ) - - return toFederatedSchema(config) + return toFederatedSchema(config = config, queries = queries) } diff --git a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/FederatedQueryResolverTest.kt b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/FederatedQueryResolverTest.kt index 095cc06206..15e152151a 100644 --- a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/FederatedQueryResolverTest.kt +++ b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/FederatedQueryResolverTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package com.expediagroup.graphql.federation.execution -import graphql.ExecutionInput -import graphql.GraphQL -import org.junit.jupiter.api.Test import com.expediagroup.graphql.federation.data.BookResolver import com.expediagroup.graphql.federation.data.UserResolver import com.expediagroup.graphql.federation.data.federatedTestSchema +import graphql.ExecutionInput +import graphql.GraphQL +import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -47,7 +47,9 @@ class FederatedQueryResolverTest { @Test fun `verify can resolve federated entities`() { - val schema = federatedTestSchema(mapOf("Book" to BookResolver(), "User" to UserResolver())) + val schema = federatedTestSchema( + federatedTypeResolvers = mapOf("Book" to BookResolver(), "User" to UserResolver()) + ) val userRepresentation = mapOf("__typename" to "User", "userId" to 123, "name" to "testName") val book1Representation = mapOf("__typename" to "Book", "id" to 987, "weight" to 2.0) val book2Representation = mapOf("__typename" to "Book", "id" to 988, "weight" to 1.0) diff --git a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/ServiceQueryResolverTest.kt b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/ServiceQueryResolverTest.kt index 7bc5a7f140..0350cd5132 100644 --- a/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/ServiceQueryResolverTest.kt +++ b/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/ServiceQueryResolverTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,12 @@ package com.expediagroup.graphql.federation.execution import com.expediagroup.graphql.TopLevelObject import com.expediagroup.graphql.federation.FederatedSchemaGeneratorConfig import com.expediagroup.graphql.federation.FederatedSchemaGeneratorHooks +import com.expediagroup.graphql.federation.data.queries.simple.NestedQuery +import com.expediagroup.graphql.federation.data.queries.simple.SimpleQuery import com.expediagroup.graphql.federation.toFederatedSchema import graphql.ExecutionInput import graphql.GraphQL import org.junit.jupiter.api.Test -import com.expediagroup.graphql.federation.data.queries.simple.NestedQuery -import com.expediagroup.graphql.federation.data.queries.simple.SimpleQuery import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -76,7 +76,7 @@ class ServiceQueryResolverTest { hooks = FederatedSchemaGeneratorHooks(FederatedTypeRegistry()) ) - val schema = toFederatedSchema(config) + val schema = toFederatedSchema(config = config) val query = """ query sdlQuery { _service { diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInputObjectTypeException.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInputObjectTypeException.kt new file mode 100644 index 0000000000..ded2c4f9b5 --- /dev/null +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInputObjectTypeException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.exceptions + +import com.expediagroup.graphql.generator.extensions.getSimpleName +import kotlin.reflect.KType + +/** + * 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 + * any defined fields would result in a field that is impossible to query without producing an error. + * + * @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568) + */ +class EmptyInputObjectTypeException(ktype: KType) : GraphQLKotlinException("Invalid ${ktype.getSimpleName(isInputType = true)} input object type - input object does not expose any fields.") diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInterfaceTypeException.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInterfaceTypeException.kt new file mode 100644 index 0000000000..77b6e22b0c --- /dev/null +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyInterfaceTypeException.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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.exceptions + +import com.expediagroup.graphql.generator.extensions.getSimpleName +import kotlin.reflect.KType + +/** + * Thrown when interface type does not expose any fields - this should never happen unless we explicitly exclude fields from interface either through + * annotating fields with @GraphQLIgnore or by custom hooks that filter out available functions. Since GraphQL always requires you to select fields down + * to scalar values, an object type without any defined fields cannot be accessed in any way in a query. + * + * @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568) + */ +class EmptyInterfaceTypeException(ktype: KType) : GraphQLKotlinException("Invalid ${ktype.getSimpleName()} interface type - interface does not expose any fields.") diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyMutationTypeException.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyMutationTypeException.kt new file mode 100644 index 0000000000..05aae1defc --- /dev/null +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyMutationTypeException.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 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.exceptions + +/** + * Thrown when generated GraphQL Mutation type does not expose any fields. + * + * @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568) + */ +object EmptyMutationTypeException : GraphQLKotlinException("Invalid mutation object type - no valid mutations are available.") diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyObjectTypeException.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyObjectTypeException.kt new file mode 100644 index 0000000000..89de916787 --- /dev/null +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyObjectTypeException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.exceptions + +import com.expediagroup.graphql.generator.extensions.getSimpleName +import kotlin.reflect.KType + +/** + * 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 + * in any way in a query. + * + * @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568) + */ +class EmptyObjectTypeException(ktype: KType) : GraphQLKotlinException("Invalid ${ktype.getSimpleName()} object type - object does not expose any fields.") diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyQueryTypeException.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyQueryTypeException.kt new file mode 100644 index 0000000000..2c09342266 --- /dev/null +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptyQueryTypeException.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 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.exceptions + +/** + * Thrown when generated GraphQL Query type does not expose any fields. + * + * @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568) + */ +object EmptyQueryTypeException : GraphQLKotlinException("Invalid query object type - no valid queries are available.") diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptySubscriptionTypeException.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptySubscriptionTypeException.kt new file mode 100644 index 0000000000..c2ce3eae82 --- /dev/null +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/EmptySubscriptionTypeException.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 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.exceptions + +/** + * Thrown when generated GraphQL Subscription type does not expose any fields. + * + * @see [Issue 568](https://github.com/graphql/graphql-spec/issues/568) + */ +object EmptySubscriptionTypeException : GraphQLKotlinException("Invalid subscription object type - no valid subscriptions are available.") diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/InvalidInterfaceException.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/InvalidInterfaceException.kt index 6b1e49377c..5802e00041 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/InvalidInterfaceException.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/exceptions/InvalidInterfaceException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package com.expediagroup.graphql.exceptions +import kotlin.reflect.KClass + /** - * Thrown when a interface implements another interface or abstract class that is not exluded from the schema. + * Thrown when an interface implements another interface or abstract class that is not excluded from the schema. * * This is an invalid schema until the GraphQL spec is updated * https://github.com/ExpediaGroup/graphql-kotlin/issues/419 */ -class InvalidInterfaceException : GraphQLKotlinException("Interfaces can not have any superclasses") +class InvalidInterfaceException(klazz: KClass<*>) : GraphQLKotlinException("Invalid ${klazz.simpleName} interface - interfaces can not have any superclasses.") diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/InterfaceBuilder.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/InterfaceBuilder.kt index 7769e2a832..541eb0f0c3 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/InterfaceBuilder.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/InterfaceBuilder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ import kotlin.reflect.full.createType internal fun generateInterface(generator: SchemaGenerator, kClass: KClass<*>): GraphQLInterfaceType { // Interfaces can not implement another interface in GraphQL if (kClass.getValidSuperclasses(generator.config.hooks).isNotEmpty()) { - throw InvalidInterfaceException() + throw InvalidInterfaceException(klazz = kClass) } val builder = GraphQLInterfaceType.newInterface() diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/MutationBuilder.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/MutationBuilder.kt index 91f3ef5f23..1dd578cc3e 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/MutationBuilder.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/MutationBuilder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import com.expediagroup.graphql.generator.extensions.isNotPublic import graphql.schema.GraphQLObjectType fun generateMutations(generator: SchemaGenerator, mutations: List): GraphQLObjectType? { - if (mutations.isEmpty()) { return null } @@ -44,10 +43,10 @@ fun generateMutations(generator: SchemaGenerator, mutations: List): query.kClass.getValidFunctions(generator.config.hooks) .forEach { val function = generateFunction(generator, it, generator.config.topLevelNames.query, query.obj) - val functionFromHook = generator.config.hooks.didGenerateQueryType(query.kClass, it, function) + val functionFromHook = generator.config.hooks.didGenerateQueryField(query.kClass, it, function) queryBuilder.field(functionFromHook) } } - return queryBuilder.build() + return generator.config.hooks.didGenerateQueryObject(queryBuilder.build()) } diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilder.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilder.kt index 716ec93fc0..30439cafbe 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilder.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import graphql.schema.GraphQLObjectType import org.reactivestreams.Publisher internal fun generateSubscriptions(generator: SchemaGenerator, subscriptions: List): GraphQLObjectType? { - if (subscriptions.isEmpty()) { return null } @@ -46,10 +45,10 @@ internal fun generateSubscriptions(generator: SchemaGenerator, subscriptions: Li } val function = generateFunction(generator, it, generator.config.topLevelNames.subscription, subscription.obj) - val functionFromHook = generator.config.hooks.didGenerateSubscriptionType(subscription.kClass, it, function) + val functionFromHook = generator.config.hooks.didGenerateSubscriptionField(subscription.kClass, it, function) subscriptionBuilder.field(functionFromHook) } } - return subscriptionBuilder.build() + return generator.config.hooks.didGenerateSubscriptionObject(subscriptionBuilder.build()) } diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooks.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooks.kt index a4b39c1828..a76f6f24b4 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooks.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooks.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,21 @@ package com.expediagroup.graphql.hooks import com.expediagroup.graphql.directives.KotlinDirectiveWiringFactory +import com.expediagroup.graphql.exceptions.EmptyInputObjectTypeException +import com.expediagroup.graphql.exceptions.EmptyInterfaceTypeException +import com.expediagroup.graphql.exceptions.EmptyMutationTypeException +import com.expediagroup.graphql.exceptions.EmptyObjectTypeException +import com.expediagroup.graphql.exceptions.EmptyQueryTypeException +import com.expediagroup.graphql.exceptions.EmptySubscriptionTypeException import graphql.schema.FieldCoordinates import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLInputObjectType +import graphql.schema.GraphQLInterfaceType +import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema import graphql.schema.GraphQLType +import graphql.schema.GraphQLTypeUtil import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KProperty @@ -90,22 +100,58 @@ interface SchemaGeneratorHooks { /** * Called after wrapping the type based on nullity but before adding the generated type to the schema */ - fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType) = generatedType + @Suppress("Detekt.ThrowsCount") + fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType { + val unwrapped = GraphQLTypeUtil.unwrapNonNull(generatedType) + return when { + unwrapped is GraphQLInterfaceType && unwrapped.fieldDefinitions.isEmpty() -> throw EmptyInterfaceTypeException(ktype = type) + unwrapped is GraphQLObjectType && unwrapped.fieldDefinitions.isEmpty() -> throw EmptyObjectTypeException(ktype = type) + unwrapped is GraphQLInputObjectType && unwrapped.fieldDefinitions.isEmpty() -> throw EmptyInputObjectTypeException(ktype = type) + else -> generatedType + } + } /** - * Called after converting the function to a field definition but before adding to the schema to allow customization + * Called after converting the function to a field definition but before adding to the query object to allow customization */ - fun didGenerateQueryType(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition + fun didGenerateQueryField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition /** - * Called after converting the function to a field definition but before adding to the schema to allow customization + * Called after converting the function to a field definition but before adding to the mutation object to allow customization */ - fun didGenerateMutationType(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition + fun didGenerateMutationField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition /** - * Called after converting the function to a field definition but before adding to the schema to allow customization + * Called after converting the function to a field definition but before adding to the subscription object to allow customization */ - fun didGenerateSubscriptionType(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition + fun didGenerateSubscriptionField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition = fieldDefinition + + /** + * Called after generating the Query object but before adding it to the schema. + */ + fun didGenerateQueryObject(type: GraphQLObjectType): GraphQLObjectType = if (type.fieldDefinitions.isEmpty()) { + throw EmptyQueryTypeException + } else { + type + } + + /** + * Called after generating the Mutation object but before adding it to the schema. + */ + fun didGenerateMutationObject(type: GraphQLObjectType): GraphQLObjectType = if (type.fieldDefinitions.isEmpty()) { + throw EmptyMutationTypeException + } else { + type + } + + /** + * Called after generating the Subscription object but before adding it to the schema. + */ + fun didGenerateSubscriptionObject(type: GraphQLObjectType): GraphQLObjectType = if (type.fieldDefinitions.isEmpty()) { + throw EmptySubscriptionTypeException + } else { + type + } val wiringFactory: KotlinDirectiveWiringFactory get() = KotlinDirectiveWiringFactory() diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/PolymorphicTests.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/PolymorphicTests.kt index c28d47be62..dce5dfc3fa 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/PolymorphicTests.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/PolymorphicTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -269,4 +269,7 @@ class Cheesecake : Cake { interface Dessert -class IceCream : Dessert +@Suppress("Detekt.FunctionOnlyReturningConstant") +class IceCream : Dessert { + fun flavor(): String = "chocolate" +} diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/MutationBuilderTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/MutationBuilderTest.kt index 8067b7d2dc..92fe276ae6 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/MutationBuilderTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/MutationBuilderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,14 @@ package com.expediagroup.graphql.generator.types import com.expediagroup.graphql.TopLevelObject import com.expediagroup.graphql.annotations.GraphQLDescription import com.expediagroup.graphql.annotations.GraphQLIgnore +import com.expediagroup.graphql.exceptions.EmptyMutationTypeException import com.expediagroup.graphql.exceptions.InvalidMutationTypeException import com.expediagroup.graphql.generator.extensions.isTrue import com.expediagroup.graphql.hooks.SchemaGeneratorHooks import com.expediagroup.graphql.test.utils.SimpleDirective import graphql.schema.GraphQLFieldDefinition import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.test.assertEquals @@ -38,9 +40,9 @@ internal class MutationBuilderTest : TypeTestHelper() { internal class SimpleHooks : SchemaGeneratorHooks { var calledHook = false - override fun didGenerateMutationType(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition { + override fun didGenerateMutationField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition { calledHook = true - return super.didGenerateMutationType(kClass, function, fieldDefinition) + return super.didGenerateMutationField(kClass, function, fieldDefinition) } } @@ -78,9 +80,9 @@ internal class MutationBuilderTest : TypeTestHelper() { @Test fun `mutation with no valid functions`() { val mutations = listOf(TopLevelObject(NoFunctions())) - val result = generateMutations(generator, mutations) - assertEquals(expected = "TestTopLevelMutation", actual = result?.name) - assertTrue(result?.fieldDefinitions?.isEmpty().isTrue()) + assertThrows { + generateMutations(generator, mutations) + } } @Test diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/QueryBuilderTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/QueryBuilderTest.kt index 4ba0c64d60..b9f38c83ea 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/QueryBuilderTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/QueryBuilderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,14 @@ package com.expediagroup.graphql.generator.types import com.expediagroup.graphql.TopLevelObject import com.expediagroup.graphql.annotations.GraphQLDescription import com.expediagroup.graphql.annotations.GraphQLIgnore +import com.expediagroup.graphql.exceptions.EmptyQueryTypeException import com.expediagroup.graphql.exceptions.InvalidQueryTypeException import com.expediagroup.graphql.generator.extensions.isTrue import com.expediagroup.graphql.hooks.SchemaGeneratorHooks import com.expediagroup.graphql.test.utils.SimpleDirective import graphql.schema.GraphQLFieldDefinition import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.test.assertEquals @@ -37,9 +39,9 @@ internal class QueryBuilderTest : TypeTestHelper() { internal class SimpleHooks : SchemaGeneratorHooks { var calledHook = false - override fun didGenerateQueryType(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition { + override fun didGenerateQueryField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition { calledHook = true - return super.didGenerateQueryType(kClass, function, fieldDefinition) + return super.didGenerateQueryField(kClass, function, fieldDefinition) } } @@ -72,9 +74,16 @@ internal class QueryBuilderTest : TypeTestHelper() { @Test fun `query with no valid functions`() { val queries = listOf(TopLevelObject(NoFunctions())) - val result = generateQueries(generator, queries) - assertEquals(expected = "TestTopLevelQuery", actual = result.name) - assertTrue(result.fieldDefinitions.isEmpty()) + assertThrows { + generateQueries(generator, queries) + } + } + + @Test + fun `verify builder fails if no queries are specified`() { + assertThrows { + generateQueries(generator, emptyList()) + } } @Test diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilderTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilderTest.kt index d89e340db5..ac0e31b1c1 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilderTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/SubscriptionBuilderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,14 @@ package com.expediagroup.graphql.generator.types import com.expediagroup.graphql.TopLevelNames import com.expediagroup.graphql.TopLevelObject +import com.expediagroup.graphql.exceptions.EmptySubscriptionTypeException import com.expediagroup.graphql.exceptions.InvalidSubscriptionTypeException import com.expediagroup.graphql.hooks.SchemaGeneratorHooks import graphql.schema.GraphQLFieldDefinition import io.mockk.every import io.reactivex.Flowable import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.reactivestreams.Publisher import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -40,6 +42,13 @@ internal class SubscriptionBuilderTest : TypeTestHelper() { assertNull(result) } + @Test + fun `given subscription without fields should throw exception`() { + assertThrows { + generateSubscriptions(generator, listOf(TopLevelObject(MyEmptyTestSubscription()))) + } + } + @Test fun `give a valid subscription class, it should properly set the top level name`() { val subscriptions = listOf(TopLevelObject(MyPublicTestSubscription())) @@ -100,7 +109,7 @@ internal class SubscriptionBuilderTest : TypeTestHelper() { val subscriptions = listOf(TopLevelObject(MyPublicTestSubscription())) class CustomHooks : SchemaGeneratorHooks { - override fun didGenerateSubscriptionType(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition { + override fun didGenerateSubscriptionField(kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition): GraphQLFieldDefinition { return if (fieldDefinition.name == "filterMe") { fieldDefinition.transform { fieldBuilder -> fieldBuilder.name("changedField") } } else fieldDefinition @@ -132,3 +141,5 @@ class MyInvalidSubscriptionClass { private class MyPrivateTestSubscription { fun counter(): Publisher = Flowable.just(3) } + +class MyEmptyTestSubscription diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/UnionBuilderTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/UnionBuilderTest.kt index 52b4b821c7..8f514f673d 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/UnionBuilderTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/UnionBuilderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,14 +34,20 @@ internal class UnionBuilderTest : TypeTestHelper() { @SimpleDirective private interface Cake + @Suppress("Detekt.FunctionOnlyReturningConstant") @GraphQLDescription("so red") - private class StrawBerryCake : Cake + private class StrawBerryCake : Cake { + fun recipe(): String = "google it" + } @GraphQLName("CakeRenamed") private interface CakeCustomName + @Suppress("Detekt.FunctionOnlyReturningConstant") @GraphQLName("StrawBerryCakeRenamed") - private class StrawBerryCakeCustomName : CakeCustomName + private class StrawBerryCakeCustomName : CakeCustomName { + fun recipe(): String = "bing it" + } private interface NestedUnionA diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt index 6fb9f8aae8..23d16fbad4 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/hooks/SchemaGeneratorHooksTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,13 @@ package com.expediagroup.graphql.hooks import com.expediagroup.graphql.TopLevelObject +import com.expediagroup.graphql.annotations.GraphQLIgnore +import com.expediagroup.graphql.exceptions.EmptyInputObjectTypeException +import com.expediagroup.graphql.exceptions.EmptyInterfaceTypeException +import com.expediagroup.graphql.exceptions.EmptyObjectTypeException import com.expediagroup.graphql.generator.extensions.getSimpleName import com.expediagroup.graphql.getTestSchemaConfigWithHooks +import com.expediagroup.graphql.testSchemaConfig import com.expediagroup.graphql.toSchema import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLInterfaceType @@ -26,6 +31,7 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema import graphql.schema.GraphQLType import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.random.Random import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -68,6 +74,9 @@ class SchemaGeneratorHooksTest { calledFilterFunction = true return false } + + // skip validation + override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType = generatedType } val hooks = MockSchemaGeneratorHooks() @@ -89,6 +98,12 @@ class SchemaGeneratorHooksTest { calledFilterFunction = true return false } + + // skip validation + override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType = generatedType + + // skip validation + override fun didGenerateQueryObject(type: GraphQLObjectType): GraphQLObjectType = type } val hooks = MockSchemaGeneratorHooks() @@ -120,6 +135,33 @@ class SchemaGeneratorHooksTest { assertTrue(hooks.seenTypes.contains(OtherData::class.createType())) } + @Test + fun `empty object type will not be added to the schema`() { + assertThrows { + toSchema( + queries = listOf(TopLevelObject(TestWithEmptyObjectQuery())), + config = testSchemaConfig) + } + } + + @Test + fun `empty input object type will not be added to the schema`() { + assertThrows { + toSchema( + queries = listOf(TopLevelObject(TestWithEmptyInputObjectQuery())), + config = testSchemaConfig) + } + } + + @Test + fun `empty interface will not be added to the schema`() { + assertThrows { + toSchema( + queries = listOf(TopLevelObject(TestWithEmptyInterfaceQuery())), + config = testSchemaConfig) + } + } + @Test fun `calls hook before adding type to schema`() { class MockSchemaGeneratorHooks : SchemaGeneratorHooks { @@ -153,18 +195,15 @@ class SchemaGeneratorHooksTest { } @Test - fun `calls hook before adding query to schema`() { + fun `calls hook before adding query field to schema`() { class MockSchemaGeneratorHooks : SchemaGeneratorHooks { - override fun didGenerateQueryType( + override fun didGenerateQueryField( kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition ): GraphQLFieldDefinition { - val newField = GraphQLFieldDefinition.Builder() + val newField = GraphQLFieldDefinition.Builder(fieldDefinition) newField.description("Hijacked Description") - newField.name(fieldDefinition.name) - newField.type(fieldDefinition.type) - newField.arguments(fieldDefinition.arguments) return newField.build() } } @@ -180,18 +219,15 @@ class SchemaGeneratorHooksTest { } @Test - fun `calls hook before adding mutation to schema`() { + fun `calls hook before adding mutation field to schema`() { class MockSchemaGeneratorHooks : SchemaGeneratorHooks { - override fun didGenerateMutationType( + override fun didGenerateMutationField( kClass: KClass<*>, function: KFunction<*>, fieldDefinition: GraphQLFieldDefinition ): GraphQLFieldDefinition { - val newField = GraphQLFieldDefinition.Builder() + val newField = GraphQLFieldDefinition.Builder(fieldDefinition) newField.description("Hijacked Description") - newField.name(fieldDefinition.name) - newField.type(fieldDefinition.type) - newField.arguments(fieldDefinition.arguments) return newField.build() } } @@ -234,4 +270,28 @@ class SchemaGeneratorHooksTest { data class SomeData(override val id: String, val someNumber: Int) : RandomData data class OtherData(override val id: String, val otherNumber: Int) : RandomData + + class TestWithEmptyObjectQuery { + fun emptyObject(): EmptyData = EmptyData() + } + + class EmptyData + + class TestWithEmptyInputObjectQuery { + fun emptyObject(input: PrivateData) = input + } + + @Suppress("Detekt.UnusedPrivateMember") + data class PrivateData(private val id: String) + + class TestWithEmptyInterfaceQuery { + fun emptyInterface(): EmptyInterface = EmptyImplementation("123") + } + + interface EmptyInterface { + @GraphQLIgnore + val id: String + } + + class EmptyImplementation(override val id: String) : EmptyInterface } diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SubscriptionConfigurationTest.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SubscriptionConfigurationTest.kt index 678981835c..9cdc0a2cd9 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SubscriptionConfigurationTest.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SubscriptionConfigurationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package com.expediagroup.graphql.spring import com.expediagroup.graphql.SchemaGeneratorConfig import com.expediagroup.graphql.spring.execution.QueryHandler import com.expediagroup.graphql.spring.execution.SubscriptionHandler +import com.expediagroup.graphql.spring.operations.Query import com.expediagroup.graphql.spring.operations.Subscription import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -102,6 +103,9 @@ class SubscriptionConfigurationTest { @Bean fun objectMapper(): ObjectMapper = jacksonObjectMapper() + @Bean + fun query(): Query = SimpleQuery() + @Bean fun subscription(): Subscription = SimpleSubscription() } @@ -113,6 +117,9 @@ class SubscriptionConfigurationTest { @Bean fun objectMapper(): ObjectMapper = jacksonObjectMapper() + @Bean + fun query(): Query = SimpleQuery() + @Bean fun subscription(): Subscription = SimpleSubscription() @@ -125,6 +132,13 @@ class SubscriptionConfigurationTest { fun webSocketHandlerAdapter(): WebSocketHandlerAdapter = mockk() } + // GraphQL spec requires at least single query to be present as Query type is needed to run introspection queries + // see: https://github.com/graphql/graphql-spec/issues/490 and https://github.com/graphql/graphql-spec/issues/568 + class SimpleQuery : Query { + @Suppress("Detekt.FunctionOnlyReturningConstant") + fun query(): String = "hello!" + } + class SimpleSubscription : Subscription { fun ticker(): Flux = Flux.range(1, 5) .delayElements(Duration.ofMillis(100)) diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt index d3778a399f..e2d2055d9a 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ class SubscriptionHandlerTest { private val testSchema: GraphQLSchema = toSchema( config = SchemaGeneratorConfig(supportedPackages = listOf("com.expediagroup.graphql.spring.execution")), - queries = listOf(), + queries = listOf(TopLevelObject(BasicQuery())), subscriptions = listOf(TopLevelObject(BasicSubscription())) ) private val testGraphQL: GraphQL = GraphQL.newGraphQL(testSchema).build() @@ -105,6 +105,13 @@ class SubscriptionHandlerTest { .verify() } + // GraphQL spec requires at least single query to be present as Query type is needed to run introspection queries + // see: https://github.com/graphql/graphql-spec/issues/490 and https://github.com/graphql/graphql-spec/issues/568 + class BasicQuery { + @Suppress("Detekt.FunctionOnlyReturningConstant") + fun query(): String = "hello" + } + class BasicSubscription { fun ticker(): Flux = Flux.range(1, 5) .delayElements(Duration.ofMillis(100)) diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt index 0e3b75f66c..1c5b6c9c42 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import com.expediagroup.graphql.spring.model.GraphQLRequest import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ClientMessages.GQL_START +import com.expediagroup.graphql.spring.operations.Query import com.expediagroup.graphql.spring.operations.Subscription import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue @@ -161,6 +162,10 @@ class SubscriptionWebSocketHandlerIT(@LocalServerPort private var port: Int) { @Configuration class TestConfiguration { + + @Bean + fun query(): Query = SimpleQuery() + @Bean fun subscription(): Subscription = SimpleSubscription() @@ -172,6 +177,13 @@ class SubscriptionWebSocketHandlerIT(@LocalServerPort private var port: Int) { } } + // GraphQL spec requires at least single query to be present as Query type is needed to run introspection queries + // see: https://github.com/graphql/graphql-spec/issues/490 and https://github.com/graphql/graphql-spec/issues/568 + class SimpleQuery : Query { + @Suppress("Detekt.FunctionOnlyReturningConstant") + fun query(): String = "hello!" + } + class SimpleSubscription : Subscription { private val characters = listOf("Alice", "Bob", "Chuck", "Dave", "Eve")