diff --git a/pom.xml b/pom.xml index 093e689c..cd1a5311 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 2.3.10 1.10.2 2.22.3 - 25.1 + 26.1 1.0.4 ${java.version} diff --git a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt index d7c40b6e..301dc411 100644 --- a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt @@ -22,7 +22,7 @@ internal abstract class DictionaryTypeResolver( } override fun getType(env: TypeResolutionEnvironment): GraphQLObjectType? { - val clazz = env.getObject().javaClass + val clazz = env.getObject()!!.javaClass val name = getTypeDefinition(clazz)?.name ?: clazz.simpleName return env.schema.getObjectType(name) ?: throw TypeResolverError(getError(name)) } diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaObjects.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaObjects.kt index efc53693..0c4744fa 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaObjects.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaObjects.kt @@ -14,6 +14,10 @@ data class SchemaObjects( val codeRegistryBuilder: GraphQLCodeRegistry.Builder, val description: String? ) { + // TODO change dictionary to Set in the next major version and remove this cast + @Suppress("UNCHECKED_CAST") + private fun namedDictionary(): Set = dictionary as Set + /** * Makes a GraphQLSchema with query, mutation and subscription. */ @@ -23,7 +27,7 @@ data class SchemaObjects( .query(query) .mutation(mutation) .subscription(subscription) - .additionalTypes(dictionary) + .additionalTypes(namedDictionary()) .additionalDirectives(directives) .codeRegistry(codeRegistryBuilder.build()) .build() @@ -35,6 +39,6 @@ data class SchemaObjects( fun toReadOnlySchema(): GraphQLSchema = GraphQLSchema.newSchema() .description(description) .query(query) - .additionalTypes(dictionary) + .additionalTypes(namedDictionary()) .build() } diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt index c26efc81..d2049ada 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt @@ -1,5 +1,6 @@ package graphql.kickstart.tools +import graphql.Directives import graphql.introspection.Introspection import graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION import graphql.kickstart.tools.directive.DirectiveWiringHelper @@ -8,7 +9,6 @@ import graphql.kickstart.tools.util.getExtendedFieldDefinitions import graphql.kickstart.tools.util.unwrap import graphql.language.* import graphql.schema.* -import graphql.schema.idl.DirectiveInfo import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.ScalarInfo import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility @@ -351,7 +351,7 @@ class SchemaParser internal constructor( private fun buildAppliedDirectives(directives: List): Array { return directives.map { directive -> val graphQLDirective = schemaDirectives.find { d -> d.name == directive.name } - ?: DirectiveInfo.GRAPHQL_SPECIFICATION_DIRECTIVE_MAP[directive.name] + ?: BUILT_IN_DIRECTIVES[directive.name] ?: throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.") val graphQLArguments = graphQLDirective.arguments.associateBy { it.name } @@ -374,6 +374,22 @@ class SchemaParser internal constructor( ) } } + .apply { + // a bare @deprecated has no "reason" argument, which makes SchemaPrinter throw a NPE. + // copy the default from the directive definition (for the built-in one: "No longer supported"). + if (directive.name == Directives.DeprecatedDirective.name && directive.arguments.none { it.name == "reason" }) { + val reasonArgument = graphQLArguments["reason"] + if (reasonArgument != null && reasonArgument.hasSetDefaultValue()) { + argument(GraphQLAppliedDirectiveArgument.newArgument() + .name(reasonArgument.name) + .type(reasonArgument.type) + .description(reasonArgument.description) + .inputValueWithState(reasonArgument.argumentDefaultValue) + .build() + ) + } + } + } .build() }.toTypedArray() } @@ -391,7 +407,7 @@ class SchemaParser internal constructor( if (repeatable || !names.contains(directive.name)) { names.add(directive.name) val graphQLDirective = this.schemaDirectives.find { d -> d.name == directive.name } - ?: DirectiveInfo.GRAPHQL_SPECIFICATION_DIRECTIVE_MAP[directive.name] + ?: BUILT_IN_DIRECTIVES[directive.name] ?: throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.") val graphQLArguments = graphQLDirective.arguments.associateBy { it.name } output.add( @@ -521,4 +537,16 @@ class SchemaError(message: String, cause: Throwable? = null) : RuntimeException( val GRAPHQL_SCALARS = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS.associateBy { it.name } +// Built from the individual constants rather than Directives.BUILT_IN_DIRECTIVES_MAP (added in graphql-java 26) +// so that the library keeps working with graphql-java 25. TODO replace with Directives.BUILT_IN_DIRECTIVES_MAP once we deploy a major version +private val BUILT_IN_DIRECTIVES = listOf( + Directives.IncludeDirective, + Directives.SkipDirective, + Directives.DeprecatedDirective, + Directives.SpecifiedByDirective, + Directives.OneOfDirective, + Directives.DeferDirective, + Directives.ExperimentalDisableErrorPropagationDirective +).associateBy { it.name } + const val DEFAULT_DEPRECATION_MESSAGE = "No longer supported" diff --git a/src/main/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactory.kt b/src/main/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactory.kt index b3e05db6..465d5bc4 100644 --- a/src/main/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactory.kt +++ b/src/main/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactory.kt @@ -1,5 +1,6 @@ package graphql.kickstart.tools.relay +import graphql.kickstart.tools.SchemaError import graphql.kickstart.tools.TypeDefinitionFactory import graphql.language.* @@ -54,7 +55,7 @@ class RelayConnectionFactory : TypeDefinitionFactory { .fieldDefinition(FieldDefinition("pageInfo", TypeName("PageInfo"))) .build() - private fun createEdgeDefinition(connectionType: String, nodeType: String?): ObjectTypeDefinition = + private fun createEdgeDefinition(connectionType: String, nodeType: String): ObjectTypeDefinition = ObjectTypeDefinition.newObjectTypeDefinition() .name(connectionType + "Edge") .fieldDefinition(FieldDefinition("cursor", TypeName("String"))) @@ -70,9 +71,9 @@ class RelayConnectionFactory : TypeDefinitionFactory { .fieldDefinition(FieldDefinition("endCursor", TypeName("String"))) .build() - private fun Directive.forTypeName(): String? { - return (this.getArgument("for").value as StringValue).value - } + private fun Directive.forTypeName(): String = + (getArgument("for")?.value as? StringValue)?.value + ?: throw SchemaError("@connection directive is missing the required 'for' argument") private fun Directive.withField(field: FieldDefinition): DirectiveWithField { return DirectiveWithField(field, this.name, this.arguments, this.sourceLocation, this.comments) @@ -92,9 +93,9 @@ class RelayConnectionFactory : TypeDefinitionFactory { fun getTypeName(): String { val type = field.type if (type is NonNullType) { - return (type.type as TypeName).name + return (type.type as TypeName).name!! } - return (field.type as TypeName).name + return (field.type as TypeName).name!! } } } diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt index 76fc7f19..3a0ccd11 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt @@ -141,7 +141,7 @@ internal class MethodFieldResolver( is ListType -> List::class.java.isAssignableFrom(this.genericType.getRawClass(genericParameterType)) && isConcreteScalarType(environment, type.type, this.genericType.unwrapGenericType(genericParameterType)) - is TypeName -> environment.graphQLSchema?.getType(type.name)?.let { isScalar(it) && type.name != "ID" } + is TypeName -> environment.graphQLSchema?.getType(type.name!!)?.let { isScalar(it) && type.name != "ID" } ?: false is NonNullType -> isConcreteScalarType(environment, type.type, genericParameterType) diff --git a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt index e61e88c9..7a646182 100644 --- a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt +++ b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt @@ -52,7 +52,7 @@ internal val Class<*>.declaredNonProxyMethods: List internal fun getDocumentation(node: AbstractNode<*>, options: SchemaParserOptions): String? = when { - node is AbstractDescribedNode<*> && node.description != null -> node.description.content + node is AbstractDescribedNode<*> && node.description != null -> node.description?.content !options.useCommentsForDescriptions -> null node.comments.isNullOrEmpty() -> null else -> node.comments.asSequence() diff --git a/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt b/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt index 8871e662..7707f2bc 100644 --- a/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt @@ -18,8 +18,8 @@ class PlaceTest { val executionInput = ExecutionInput.newExecutionInput().query(query).build() val result = graphql.execute(executionInput) - assert(result.getData>>()["places1"]?.size == 3) - assert(result.getData>>()["places2"]?.size == 2) + assert(result.getData>>()!!["places1"]?.size == 3) + assert(result.getData>>()!!["places2"]?.size == 2) } } diff --git a/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt index b611a06c..0acec3bb 100644 --- a/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt @@ -6,6 +6,7 @@ import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLInputObjectType import graphql.schema.GraphQLObjectType +import graphql.schema.idl.SchemaPrinter import org.junit.Test class DeprecatedDirectiveTest { @@ -111,7 +112,7 @@ class DeprecatedDirectiveTest { val userTypeEnum = schema.getType("UserType") as GraphQLEnumType val droidValue = userTypeEnum.getValue("DROID") - assert(droidValue.isDeprecated) + assert(droidValue!!.isDeprecated) assertEquals(droidValue.deprecationReason, "No longer supported") } @@ -151,7 +152,7 @@ class DeprecatedDirectiveTest { val userTypeEnum = schema.getType("UserType") as GraphQLEnumType val droidValue = userTypeEnum.getValue("DROID") - assert(droidValue.isDeprecated) + assert(droidValue!!.isDeprecated) assertEquals(droidValue.deprecationReason, "This value is no longer used") } @@ -250,7 +251,7 @@ class DeprecatedDirectiveTest { .makeExecutableSchema() val directive = schema.getDirective("uppercase") - val argument = directive.getArgument("firstCharacterOnly") + val argument = directive!!.getArgument("firstCharacterOnly") assert(argument.isDeprecated) assertEquals(argument.deprecationReason, "No longer supported") @@ -285,7 +286,7 @@ class DeprecatedDirectiveTest { .makeExecutableSchema() val directive = schema.getDirective("uppercase") - val argument = directive.getArgument("firstCharacterOnly") + val argument = directive!!.getArgument("firstCharacterOnly") assert(argument.isDeprecated) assertEquals(argument.deprecationReason, "Do not use this thing") @@ -365,6 +366,48 @@ class DeprecatedDirectiveTest { assertEquals(firstField.deprecationReason, "Please do not use this field") } + @Test + fun `bare @deprecated gets the default reason and can be printed`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + users(first: Int @deprecated): UserConnection + } + + type UserConnection { + edges: [UserEdge!]! + } + + type UserEdge { + node: User! + } + + enum UserType { + JEDI + BASIC + DROID @deprecated + } + + type User { + id: ID! + name: String @deprecated + type: UserType @deprecated(reason: "custom") + } + """) + .resolvers(UsersQueryResolver()) + .build() + .makeExecutableSchema() + + val name = (schema.getType("User") as GraphQLObjectType).getField("name") + assertEquals("No longer supported", name.getAppliedDirective("deprecated").getArgument("reason")!!.getValue()) + + val printed = SchemaPrinter().print(schema) + assert(printed.contains("name: String @deprecated(reason : \"No longer supported\")")) + assert(printed.contains("type: UserType @deprecated(reason : \"custom\")")) + assert(printed.contains("DROID @deprecated(reason : \"No longer supported\")")) + } + private enum class UserType { JEDI, BASIC, diff --git a/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt index 79e787cf..8bdce91d 100644 --- a/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt @@ -275,6 +275,44 @@ class DirectiveTest { .build() } + @Test + fun `should resolve built-in directives`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + input BookFilter @oneOf { + id: Int + name: String + } + + type Book { + id: Int! + name: String! @deprecated + } + + type Query { + book(filter: BookFilter!): Book + } + """) + .resolvers(BookQueryResolver()) + .build() + .makeExecutableSchema() + + val filter = schema.getType("BookFilter") as GraphQLInputObjectType + assert(filter.isOneOf) + assertNotNull(filter.getAppliedDirective("oneOf")) + assert((schema.getType("Book") as GraphQLObjectType).getField("name").isDeprecated) + } + + private class BookQueryResolver : GraphQLQueryResolver { + fun book(filter: BookFilter): Book? = null + } + + private data class BookFilter( + val id: Int?, + val name: String? + ) + private class QueryResolver : GraphQLQueryResolver { fun books(): List { return listOf(Book(42L, "Test Book")) diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt index 2bb44147..305ef636 100644 --- a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt +++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt @@ -378,7 +378,7 @@ class Subscription : GraphQLSubscriptionResolver { fun onItemCreatedCoroutineChannel(env: DataFetchingEnvironment): ReceiveChannel { val channel = Channel(1) - channel.trySend(env.graphQlContext["newItem"]) + channel.trySend(env.graphQlContext.get("newItem")!!) return channel } @@ -393,7 +393,7 @@ class Subscription : GraphQLSubscriptionResolver { suspend fun onItemCreatedCoroutineChannelAndSuspendFunction(env: DataFetchingEnvironment): ReceiveChannel { return coroutineScope { val channel = Channel(1) - channel.trySend(env.graphQlContext["newItem"]) + channel.trySend(env.graphQlContext.get("newItem")!!) channel } } diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt index 01c56965..9539a7b1 100644 --- a/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt @@ -79,7 +79,7 @@ class EndToEndTest { .graphQLContext(mapOf("newItem" to newItem)) .variables(mapOf())) - val data = result.getData() as Publisher + val data = result.getData() as Publisher val latch = CountDownLatch(1) data.subscribe(object : Subscriber { override fun onNext(item: ExecutionResult?) { @@ -117,7 +117,7 @@ class EndToEndTest { .graphQLContext(mapOf("newItem" to newItem)) .variables(mapOf())) - val data = result.getData() as Publisher + val data = result.getData() as Publisher val latch = CountDownLatch(1) data.subscribe(object : Subscriber { override fun onNext(item: ExecutionResult?) { @@ -639,7 +639,7 @@ class EndToEndTest { .graphQLContext(mapOf("newItem" to newItem)) .variables(mapOf())) - val data = result.getData() as Publisher + val data = result.getData() as Publisher val subscriber = TestEnvironment().newManualSubscriber(data) val subscriberResult = subscriber.requestNextElement() as ExecutionResultImpl @@ -667,7 +667,7 @@ class EndToEndTest { .graphQLContext(mapOf("newItem" to newItem)) .variables(mapOf())) - val data = result.getData() as Publisher + val data = result.getData() as Publisher val subscriber = TestEnvironment().newManualSubscriber(data) val subscriberResult = subscriber.requestNextElement() as ExecutionResultImpl diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt index d5bf2fbf..f41e8e60 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt @@ -31,7 +31,7 @@ class SchemaClassScannerDirectiveTest { val value = schema.queryType.getFieldDefinition("string") .getAppliedDirective("doSomething") - .getArgument("value") + .getArgument("value")!! .getValue() assertEquals(value.value, "some thing") @@ -72,7 +72,7 @@ class SchemaClassScannerDirectiveTest { val value = schema.queryType.getFieldDefinition("string") .getAppliedDirective("doSomething") - .getArgument("value") + .getArgument("value")!! .getValue() assertEquals(value, ONE) @@ -103,7 +103,7 @@ class SchemaClassScannerDirectiveTest { val value = schema.queryType.getFieldDefinition("string") .getAppliedDirective("doSomething") - .getArgument("input") + .getArgument("input")!! .getValue>()["value"] assertEquals(value, "some value") diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt index 7d022bae..3904b380 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt @@ -259,7 +259,7 @@ class SchemaParserTest { .build() .makeExecutableSchema() - val sourceLocation = schema.getObjectType("Query") + val sourceLocation = schema.getObjectType("Query")!! .getFieldDefinition("id") .definition!!.sourceLocation assertNotNull(sourceLocation) @@ -276,7 +276,7 @@ class SchemaParserTest { .build() .makeExecutableSchema() - val sourceLocation = schema.getObjectType("Query") + val sourceLocation = schema.getObjectType("Query")!! .getFieldDefinition("id") .definition!!.sourceLocation assertNotNull(sourceLocation) @@ -505,10 +505,10 @@ class SchemaParserTest { .build() .makeExecutableSchema() - val testNonNullableArgument = schema.getObjectType("Query") + val testNonNullableArgument = schema.getObjectType("Query")!! .getFieldDefinition("testNonNullable") .arguments.first() - val testNullableArgument = schema.getObjectType("Query") + val testNullableArgument = schema.getObjectType("Query")!! .getFieldDefinition("testNullable") .arguments.first() assert(testNonNullableArgument.type is GraphQLNonNull) @@ -539,7 +539,7 @@ class SchemaParserTest { .build() .makeExecutableSchema() - val queryType = schema.getObjectType("Query") + val queryType = schema.getObjectType("Query")!! assertEquals(queryType.getFieldDefinition("description").description, "description") assertEquals(queryType.getFieldDefinition("comment").description, "comment") assertNull(queryType.getFieldDefinition("omitted").description) diff --git a/src/test/kotlin/graphql/kickstart/tools/TestUtils.kt b/src/test/kotlin/graphql/kickstart/tools/TestUtils.kt index 32473510..33da8b0b 100644 --- a/src/test/kotlin/graphql/kickstart/tools/TestUtils.kt +++ b/src/test/kotlin/graphql/kickstart/tools/TestUtils.kt @@ -19,7 +19,7 @@ fun assertNoGraphQlErrors(gql: GraphQL, args: Map = mapOf(), contex throw AssertionError("GraphQL result contained errors!\n${result.errors.map { it.message }.joinToString("\n")}") } - return result.getData() as Map + return result.getData() as Map } fun assertEquals(actual: T, expected: T) { diff --git a/src/test/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactoryTest.kt b/src/test/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactoryTest.kt index 54dc3b00..401c27fa 100644 --- a/src/test/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactoryTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/relay/RelayConnectionFactoryTest.kt @@ -1,7 +1,10 @@ package graphql.kickstart.tools.relay +import graphql.kickstart.tools.SchemaError import graphql.language.Definition +import graphql.parser.Parser import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Test class RelayConnectionFactoryTest { @@ -17,4 +20,20 @@ class RelayConnectionFactoryTest { // expect assertEquals(newDefinitions.size, 0) } + + @Test + fun `should throw schema error when @connection is missing the for argument`() { + val factory = RelayConnectionFactory() + val existing = Parser.parse( + """ + type Query { + users: UserConnection @connection + } + """ + ).definitions.toMutableList() + + val error = assertThrows(SchemaError::class.java) { factory.create(existing) } + + assertEquals("@connection directive is missing the required 'for' argument", error.message) + } }