Skip to content

Commit b96116d

Browse files
committed
Merge remote-tracking branch 'origin/master' into feature/884-reject-invalid-directives
# Conflicts: # src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt
2 parents f5b8d2c + f480042 commit b96116d

22 files changed

Lines changed: 759 additions & 676 deletions

‎.github/workflows/pull-request.yml‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,3 @@ jobs:
2424
restore-keys: ${{ runner.os }}-m2
2525
- name: Build with Maven
2626
run: mvn --batch-mode --update-snapshots verify
27-
28-
# The same jar has to work with the oldest supported graphql-java, so build and test against it too
29-
graphql-java-25:
30-
runs-on: ubuntu-latest
31-
steps:
32-
- name: Checkout
33-
uses: actions/checkout@v7
34-
- name: Setup java
35-
uses: actions/setup-java@v6
36-
with:
37-
java-version: '11'
38-
distribution: 'temurin'
39-
- name: Cache Maven packages
40-
uses: actions/cache@v6
41-
with:
42-
path: ~/.m2
43-
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
44-
restore-keys: ${{ runner.os }}-m2
45-
- name: Build with Maven
46-
run: mvn --batch-mode --update-snapshots verify -Dgraphql-java.version=25.1

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ kotlin.version=2.3.10
2626

2727
Add the dependency:
2828
```groovy
29-
compile 'io.github.graphql-java-kickstart:graphql-java-tools:14.4.0'
29+
compile 'io.github.graphql-java-kickstart:graphql-java-tools:14.5.0'
3030
```
3131

3232
### Using Maven
@@ -43,7 +43,7 @@ Add the dependency:
4343
<dependency>
4444
<groupId>io.github.graphql-java-kickstart</groupId>
4545
<artifactId>graphql-java-tools</artifactId>
46-
<version>14.4.0</version>
46+
<version>14.5.0</version>
4747
</dependency>
4848
```
4949

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>io.github.graphql-java-kickstart</groupId>
66
<artifactId>graphql-java-tools</artifactId>
7-
<version>14.4.1-SNAPSHOT</version>
7+
<version>14.5.1-SNAPSHOT</version>
88
<packaging>jar</packaging>
99

1010
<name>GraphQL Java Tools</name>

‎src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal class GenericTypeResolver(private val containingType: JavaType, private
2929
* - wildcards are replaced by their upper bound, e.g. `? extends Foo` becomes `Foo`
3030
* - primitives are replaced by their boxed class, e.g. `int` becomes `Integer`
3131
*/
32-
fun resolve(javaType: JavaType, onUnwrap: ((wrapped: ParameterizedType) -> Unit)? = null): JavaType {
32+
fun resolve(javaType: JavaType, onUnwrap: ((SchemaParserOptions.GenericWrapper) -> Unit)? = null): JavaType {
3333
return when (val type = resolveTypeVariables(javaType)) {
3434
is ParameterizedType -> {
3535
val rawType = type.rawType
@@ -41,7 +41,7 @@ internal class GenericTypeResolver(private val containingType: JavaType, private
4141
throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${wrapper.index}!")
4242
}
4343

44-
onUnwrap?.invoke(type)
44+
onUnwrap?.invoke(wrapper)
4545
val unwrapsTo = wrapper.schemaWrapper.invoke(typeArguments[wrapper.index])
4646
resolve(unwrapsTo, onUnwrap)
4747
}

‎src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt‎

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import graphql.kickstart.tools.ScanDictionary.InitialDictionaryEntry
66
import graphql.kickstart.tools.ScanDictionary.UnionMemberReference
77
import graphql.kickstart.tools.ScanDictionary.UnusedTypeReference
88
import graphql.kickstart.tools.resolver.FieldResolver
9+
import graphql.kickstart.tools.resolver.FieldResolverError
910
import graphql.kickstart.tools.resolver.FieldResolverScanner
1011
import graphql.kickstart.tools.resolver.MethodFieldResolver
1112
import graphql.kickstart.tools.resolver.PropertyFieldResolver
@@ -66,6 +67,7 @@ internal class SchemaClassScanner(
6667
private val sealedSubclassMatcher = SealedSubclassMatcher(dictionary, typeExtensions)
6768

6869
private val fieldResolversByType = mutableMapOf<ObjectTypeDefinition, MutableMap<FieldDefinition, FieldResolver>>()
70+
private val notCallableOnResolver = mutableListOf<NotCallableOnResolver>()
6971

7072
init {
7173
initialDictionary.forEach { (name, clazz) ->
@@ -145,6 +147,8 @@ internal class SchemaClassScanner(
145147
}
146148
}
147149

150+
throwIfNotCallableOnResolver()
151+
148152
return validateAndCreateResult(rootTypeHolder)
149153
}
150154

@@ -450,19 +454,18 @@ internal class SchemaClassScanner(
450454
private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) {
451455
type.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }.forEach { field ->
452456
val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)
453-
warnIfNotCallableOnResolver(type, fieldResolver)
457+
collectIfNotCallableOnResolver(type, fieldResolver)
454458

455459
fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver
456460

457461
fieldResolver.scanForMatches().forEach { potentialMatch ->
458462
dictionary.handleFoundType(typeClassMatcher.match(potentialMatch))
459463
}
460-
warnIfParameterMissing(type, fieldResolver)
464+
requireParameterForEachArgument(type, fieldResolver)
461465
}
462466
}
463467

464-
// TODO throw a FieldResolverError instead in 15.0.0 (#868)
465-
private fun warnIfNotCallableOnResolver(type: ObjectTypeDefinition, fieldResolver: FieldResolver) {
468+
private fun collectIfNotCallableOnResolver(type: ObjectTypeDefinition, fieldResolver: FieldResolver) {
466469
val source = fieldResolver.search.source ?: return
467470
val member: Member = when (fieldResolver) {
468471
is MethodFieldResolver -> fieldResolver.method
@@ -473,17 +476,26 @@ internal class SchemaClassScanner(
473476
return
474477
}
475478

476-
// class-based proxies don't delegate field reads to the target, so a field needs a getter too
477-
val fix = if (member is Field) "Add a getter for the field and use class-based proxies" else "Use class-based proxies instead"
478-
log.warn(
479+
notCallableOnResolver += NotCallableOnResolver(
479480
"Field '${type.name}.${fieldResolver.field.name}' resolves to ${member.declaringClass.name}.${member.name}, but the resolver is a " +
480-
"${source.javaClass.name}, which isn't a ${member.declaringClass.name}, so every query of the field will fail. This happens " +
481-
"with JDK dynamic proxies, which only implement interfaces. $fix, e.g. proxyTargetClass = true in Spring."
481+
"${source.javaClass.name}, which isn't a ${member.declaringClass.name}, so every query of the field will fail.",
482+
member is Field
483+
)
484+
}
485+
486+
private fun throwIfNotCallableOnResolver() {
487+
if (notCallableOnResolver.isEmpty()) {
488+
return
489+
}
490+
491+
val fix = "Use class-based proxies instead, e.g. proxyTargetClass = true in Spring." +
492+
if (notCallableOnResolver.any { it.isField }) " For a public field, also add a getter, since class-based proxies don't pass field reads to the target." else ""
493+
throw FieldResolverError(
494+
notCallableOnResolver.joinToString("\n", postfix = "\nThis happens with JDK dynamic proxies, which only implement interfaces. $fix") { it.message }
482495
)
483496
}
484497

485-
// TODO throw a FieldResolverError instead in 15.0.0 (#889)
486-
private fun warnIfParameterMissing(type: ObjectTypeDefinition, fieldResolver: FieldResolver) {
498+
private fun requireParameterForEachArgument(type: ObjectTypeDefinition, fieldResolver: FieldResolver) {
487499
if (fieldResolver !is MethodFieldResolver) {
488500
return
489501
}
@@ -495,7 +507,7 @@ internal class SchemaClassScanner(
495507
}
496508

497509
val method = fieldResolver.method
498-
log.warn(
510+
throw FieldResolverError(
499511
"Field '${type.name}.${fieldResolver.field.name}' has no parameter for its argument '${argument.name}: " +
500512
"${TypeUtil.simplePrint(argument.type)}' in ${method.declaringClass.name}.${method.name}, so the ${parameter.simpleName} " +
501513
"parameter gets the argument instead. Add a parameter for each argument, in order, before the ${parameter.simpleName} parameter."
@@ -528,6 +540,8 @@ internal class SchemaClassScanner(
528540

529541
private data class QueueItem(val type: ObjectTypeDefinition, val clazz: JavaType)
530542

543+
private data class NotCallableOnResolver(val message: String, val isField: Boolean)
544+
531545
private class RootTypesHolder(
532546
options: SchemaParserOptions,
533547
rootInfo: RootTypeInfo,

‎src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt‎

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import graphql.language.*
2323
import graphql.schema.*
2424
import graphql.schema.idl.RuntimeWiring
2525
import graphql.schema.idl.ScalarInfo
26-
import graphql.schema.visibility.BlockedFields
2726
import org.slf4j.LoggerFactory
2827
import kotlin.reflect.KClass
2928

@@ -79,13 +78,6 @@ class SchemaParser internal constructor(
7978
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
8079
*/
8180
fun parseSchemaObjects(): SchemaObjects {
82-
if (!options.introspectionEnabled) {
83-
// What the deprecated NoIntrospectionGraphqlFieldVisibility does. Its suggested replacement,
84-
// Introspection.enabledJvmWide, would also turn introspection off for every other schema in the JVM.
85-
// TODO deprecate introspectionEnabled and remove this in 15.0.0 (#858)
86-
codeRegistryBuilder.fieldVisibility(BlockedFields.newBlock().addPattern("__.*").build())
87-
}
88-
// this overrides the above introspection enabled setting obviously... todo: add documentation
8981
options.fieldVisibility?.let { codeRegistryBuilder.fieldVisibility(it) }
9082

9183
// Create GraphQL objects
@@ -172,6 +164,8 @@ class SchemaParser internal constructor(
172164
val name = objectDefinition.name
173165
val extensions = typeExtensions.extensionsOf<ObjectTypeExtensionDefinition>(name)
174166
val directives = sdlDirectives.directivesOf(objectDefinition, extensions)
167+
val fieldDefinitions = objectDefinition.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }
168+
rejectRepeated(name, "field", fieldDefinitions)
175169
val builder = GraphQLObjectType.newObject()
176170
.name(name)
177171
.definition(objectDefinition)
@@ -186,7 +180,7 @@ class SchemaParser internal constructor(
186180
}
187181
}
188182
.apply {
189-
objectDefinition.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }.forEach { fieldDefinition ->
183+
fieldDefinitions.forEach { fieldDefinition ->
190184
field { field ->
191185
createField(field, fieldDefinition, inputObjects)
192186
codeRegistryBuilder.dataFetcher(
@@ -210,6 +204,8 @@ class SchemaParser internal constructor(
210204
inputObjectsInProgress: MutableSet<String>): GraphQLInputObjectType {
211205
val extensionDefinitions = typeExtensions.extensionsOf<InputObjectTypeExtensionDefinition>(definition.name)
212206
val directives = sdlDirectives.directivesOf(definition, extensionDefinitions)
207+
val inputValueDefinitions = definition.withExtensions(typeExtensions).flatMap { it.inputValueDefinitions }
208+
rejectRepeated(definition.name, "field", inputValueDefinitions)
213209

214210
inputObjectsInProgress.add(definition.name)
215211

@@ -220,20 +216,18 @@ class SchemaParser internal constructor(
220216
.description(getDocumentation(definition, options))
221217
.withSdlDirectives(directives, INPUT_OBJECT)
222218
.apply {
223-
(extensionDefinitions + definition).forEach { typeDefinition ->
224-
typeDefinition.inputValueDefinitions.forEach { fieldDefinition ->
225-
field(
226-
GraphQLInputObjectField.newInputObjectField()
227-
.name(fieldDefinition.name)
228-
.definition(fieldDefinition)
229-
.description(getDocumentation(fieldDefinition, options))
230-
.apply { fieldDefinition.defaultValue?.let { v -> defaultValueLiteral(v) } }
231-
.apply { sdlDirectives.getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } }
232-
.type(determineInputType(fieldDefinition.type, inputObjects, inputObjectsInProgress))
233-
.withSdlDirectives(fieldDefinition.directives, INPUT_FIELD_DEFINITION)
234-
.build()
235-
)
236-
}
219+
inputValueDefinitions.forEach { fieldDefinition ->
220+
field(
221+
GraphQLInputObjectField.newInputObjectField()
222+
.name(fieldDefinition.name)
223+
.definition(fieldDefinition)
224+
.description(getDocumentation(fieldDefinition, options))
225+
.apply { fieldDefinition.defaultValue?.let { v -> defaultValueLiteral(v) } }
226+
.apply { sdlDirectives.getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } }
227+
.type(determineInputType(fieldDefinition.type, inputObjects, inputObjectsInProgress))
228+
.withSdlDirectives(fieldDefinition.directives, INPUT_FIELD_DEFINITION)
229+
.build()
230+
)
237231
}
238232
}
239233

@@ -250,15 +244,15 @@ class SchemaParser internal constructor(
250244
val extensions = typeExtensions.extensionsOf<EnumTypeExtensionDefinition>(name)
251245
val directives = sdlDirectives.directivesOf(definition, extensions)
252246
val values = definition.withExtensions(typeExtensions).flatMap { it.enumValueDefinitions }
253-
warnRepeated(name, "value", values.map { it.name })
247+
rejectRepeated(name, "value", values)
254248
val builder = GraphQLEnumType.newEnum()
255249
.name(name)
256250
.definition(definition)
257251
.extensionDefinitions(extensions)
258252
.description(getDocumentation(definition, options))
259253
.withSdlDirectives(directives, ENUM)
260254
.apply {
261-
values.distinctBy { it.name }.forEach { valueDefinition ->
255+
values.forEach { valueDefinition ->
262256
val enumName = valueDefinition.name
263257
val enumValue = if (enumClass == null) {
264258
enumName
@@ -295,12 +289,7 @@ class SchemaParser internal constructor(
295289
.specifiedByUrl(getSpecifiedByUrl(directives) ?: provided.specifiedByUrl)
296290
// like graphql-java, only the schema's directives are applied
297291
.clearDirectives()
298-
try {
299-
builder.withSdlDirectives(directives, SCALAR)
300-
} catch (e: SchemaError) {
301-
// TODO throw instead in 15.0.0 (#893), like for the other types
302-
log.warn("Scalar '$name' is built without its directives from the schema: ${e.message}")
303-
}
292+
.withSdlDirectives(directives, SCALAR)
304293

305294
return builder.build()
306295
}
@@ -309,15 +298,15 @@ class SchemaParser internal constructor(
309298
val extensions = typeExtensions.extensionsOf<InterfaceTypeExtensionDefinition>(interfaceDefinition.name)
310299
val directives = sdlDirectives.directivesOf(interfaceDefinition, extensions)
311300
val fieldDefinitions = interfaceDefinition.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }
312-
warnRepeated(interfaceDefinition.name, "field", fieldDefinitions.map { it.name })
301+
rejectRepeated(interfaceDefinition.name, "field", fieldDefinitions)
313302
val builder = GraphQLInterfaceType.newInterface()
314303
.name(interfaceDefinition.name)
315304
.definition(interfaceDefinition)
316305
.extensionDefinitions(extensions)
317306
.description(getDocumentation(interfaceDefinition, options))
318307
.withSdlDirectives(directives, INTERFACE)
319308
.apply {
320-
fieldDefinitions.distinctBy { it.name }.forEach { fieldDefinition ->
309+
fieldDefinitions.forEach { fieldDefinition ->
321310
field { field -> createField(field, fieldDefinition, inputObjects) }
322311
}
323312
}
@@ -334,7 +323,7 @@ class SchemaParser internal constructor(
334323
private fun createUnionObject(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): GraphQLUnionType {
335324
val extensions = typeExtensions.extensionsOf<UnionTypeExtensionDefinition>(definition.name)
336325
val directives = sdlDirectives.directivesOf(definition, extensions)
337-
warnRepeated(definition.name, "member", definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.map { (it as TypeName).name })
326+
rejectRepeated(definition.name, "member", definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.map { it as TypeName })
338327
val builder = GraphQLUnionType.newUnionType()
339328
.name(definition.name)
340329
.definition(definition)
@@ -376,11 +365,10 @@ class SchemaParser internal constructor(
376365
return leafObjects
377366
}
378367

379-
// TODO throw a SchemaError instead in 15.0.0 (#863), like graphql-java does
380-
private fun warnRepeated(typeName: String, kind: String, names: List<String?>) {
381-
names.groupingBy { it }.eachCount().filterValues { it > 1 }.keys.forEach {
382-
log.warn("Type '$typeName' declares $kind '$it' more than once, only the first one is kept.")
383-
}
368+
private fun rejectRepeated(typeName: String, kind: String, declarations: List<NamedNode<*>>) {
369+
val names = mutableSetOf<String?>()
370+
val repeat = declarations.firstOrNull { !names.add(it.name) } ?: return
371+
throw SchemaError("Type '$typeName' declares $kind '${repeat.name}' more than once${repeat.sourceLocation.describe()}.")
384372
}
385373

386374
private fun createField(field: GraphQLFieldDefinition.Builder, fieldDefinition: FieldDefinition, inputObjects: List<GraphQLInputObjectType>): GraphQLFieldDefinition.Builder {

‎src/main/kotlin/graphql/kickstart/tools/SchemaParserDictionary.kt‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,6 @@ class SchemaParserDictionary {
1111
*/
1212
fun toMap(): Map<String, Class<*>> = dictionary.toMap()
1313

14-
// TODO remove in 15.0.0 (#900)
15-
/**
16-
* Returns a copy of the entries added so far. If several types share a class, the inverse of the copy only maps that class
17-
* back to the first of them.
18-
*/
19-
@Deprecated("Use toMap() instead", ReplaceWith("toMap()"))
20-
@Suppress("DEPRECATION")
21-
fun getDictionary(): graphql.kickstart.tools.util.BiMap<String, Class<*>> =
22-
graphql.kickstart.tools.util.BiMap.unmodifiableCopyOf(dictionary)
23-
2414
/**
2515
* Add arbitrary classes to the parser's dictionary, overriding the generated type name.
2616
*/

‎src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ data class SchemaParserOptions internal constructor(
3131
val proxyHandlers: List<ProxyHandler>,
3232
val inputArgumentOptionalDetectOmission: Boolean,
3333
val preferGraphQLResolver: Boolean,
34-
val introspectionEnabled: Boolean,
3534
val coroutineContextProvider: CoroutineContextProvider,
3635
val typeDefinitionFactories: List<TypeDefinitionFactory>,
3736
val fieldVisibility: GraphqlFieldVisibility?,
@@ -62,7 +61,6 @@ data class SchemaParserOptions internal constructor(
6261
private val proxyHandlers: MutableList<ProxyHandler> = mutableListOf(Spring4AopProxyHandler(), GuiceAopProxyHandler(), JavassistProxyHandler(), WeldProxyHandler())
6362
private var inputArgumentOptionalDetectOmission = false
6463
private var preferGraphQLResolver = false
65-
private var introspectionEnabled = true
6664
private var coroutineContextProvider: CoroutineContextProvider? = null
6765
private var typeDefinitionFactories: MutableList<TypeDefinitionFactory> = mutableListOf(RelayConnectionFactory())
6866
private var fieldVisibility: GraphqlFieldVisibility? = null
@@ -128,10 +126,6 @@ data class SchemaParserOptions internal constructor(
128126
this.proxyHandlers.add(proxyHandler)
129127
}
130128

131-
fun introspectionEnabled(introspectionEnabled: Boolean) = this.apply {
132-
this.introspectionEnabled = introspectionEnabled
133-
}
134-
135129
fun coroutineContext(context: CoroutineContext) = this.apply {
136130
this.coroutineContextProvider = DefaultCoroutineContextProvider(context)
137131
}
@@ -217,7 +211,6 @@ data class SchemaParserOptions internal constructor(
217211
proxyHandlers,
218212
inputArgumentOptionalDetectOmission,
219213
preferGraphQLResolver,
220-
introspectionEnabled,
221214
coroutineContextProvider,
222215
typeDefinitionFactories,
223216
fieldVisibility,

0 commit comments

Comments
 (0)