Skip to content

Commit

Permalink
Merge pull request #2053 from Netflix/fixes/#2041
Browse files Browse the repository at this point in the history
Throw exception during startup when multiple entity fetchers for the same type are registered
  • Loading branch information
paulbakker authored Nov 5, 2024
2 parents 05e25f7 + d1d1283 commit b4cafa2
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2024 Netflix, 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
*
* http://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.netflix.graphql.dgs.exceptions

import java.lang.reflect.Method

class DuplicateEntityFetcherException(
val entityType: String,
val firstEntityFetcherClass: Class<out Any>,
val firstEntityFetcherMethod: Method,
val secondEntityFetcherClass: Class<out Any>,
val secondEntityFetcherMethod: Method,
) : RuntimeException(
"Duplicate EntityFetcherResolver found for entity type $entityType, defined by ${firstEntityFetcherClass.name}.${firstEntityFetcherMethod.name} and ${secondEntityFetcherClass.name}.${secondEntityFetcherMethod.name}",
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.apollographql.federation.graphqljava.Federation
import com.netflix.graphql.dgs.*
import com.netflix.graphql.dgs.exceptions.DataFetcherInputArgumentSchemaMismatchException
import com.netflix.graphql.dgs.exceptions.DataFetcherSchemaMismatchException
import com.netflix.graphql.dgs.exceptions.DuplicateEntityFetcherException
import com.netflix.graphql.dgs.exceptions.InvalidDgsConfigurationException
import com.netflix.graphql.dgs.exceptions.InvalidTypeResolverException
import com.netflix.graphql.dgs.exceptions.NoSchemaFoundException
Expand Down Expand Up @@ -582,15 +583,32 @@ class DgsSchemaProvider(
val enableInstrumentation =
method.getAnnotation(DgsEnableDataFetcherInstrumentation::class.java)?.value
?: false
val entityFetcherTypeName = dgsEntityFetcherAnnotation.name
if (enableInstrumentation) {
val coordinateName = "__entities.${dgsEntityFetcherAnnotation.name}"
val coordinateName = "__entities.$entityFetcherTypeName"
dataFetcherInfo.tracingEnabled += coordinateName
dataFetcherInfo.metricsEnabled += coordinateName
}

entityFetcherRegistry.entityFetchers[dgsEntityFetcherAnnotation.name] = dgsComponent.instance to method
// Throw if an entity fetcher for the same type was already registered
if (entityFetcherRegistry.entityFetchers.contains(entityFetcherTypeName)) {
val firstEntityFetcher = entityFetcherRegistry.entityFetchers[entityFetcherTypeName]!!

// It's possible the schema() method is invoked multiple times, so check if the second entity fetcher is different from the existing one.
if (firstEntityFetcher != dgsComponent.instance to method) {
throw DuplicateEntityFetcherException(
entityFetcherTypeName,
firstEntityFetcher.first::class.java,
firstEntityFetcher.second,
dgsComponent.instance::class.java,
method,
)
}
}

entityFetcherRegistry.entityFetchers[entityFetcherTypeName] = dgsComponent.instance to method

val type = registry.getType(dgsEntityFetcherAnnotation.name)
val type = registry.getType(entityFetcherTypeName)

if (enableEntityFetcherCustomScalarParsing) {
type.ifPresent {
Expand All @@ -604,7 +622,7 @@ class DgsSchemaProvider(
val fieldsSelection = (fields.value as StringValue).value
val paths = SelectionSetUtil.toPaths(fieldsSelection)

entityFetcherRegistry.entityFetcherInputMappings[dgsEntityFetcherAnnotation.name] =
entityFetcherRegistry.entityFetcherInputMappings[entityFetcherTypeName] =
paths
.asSequence()
.mapNotNull { path ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2024 Netflix, 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
*
* http://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.netflix.graphql.dgs

import com.netflix.graphql.dgs.DefaultDgsFederationResolverTest.Movie
import com.netflix.graphql.dgs.exceptions.DuplicateEntityFetcherException
import com.netflix.graphql.dgs.internal.DgsSchemaProvider
import com.netflix.graphql.dgs.internal.method.MethodDataFetcherFactory
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.runner.ApplicationContextRunner

class DuplicateEntityFetcherTest {
@Test
fun `Startup should fail if a duplicate EntityFetcher exists`() {
val contextRunner =
ApplicationContextRunner()
.withBean(DgsSchemaProvider::class.java)
.withBean(EntityFetcherConfig::class.java)
.withBean(MethodDataFetcherFactory::class.java)

contextRunner.run { context ->
context.start()

val exception =
catchThrowable {
context.getBean(DgsSchemaProvider::class.java).schema("type Query {}")
} as DuplicateEntityFetcherException

assertThat(
exception.message,
).contains(
"Duplicate EntityFetcherResolver found for entity type Movie",
"com.netflix.graphql.dgs.DuplicateEntityFetcherTest\$EntityFetcherConfig.movieEntityFetcher",
"com.netflix.graphql.dgs.DuplicateEntityFetcherTest\$EntityFetcherConfig.anotherMovieEntityFetcher",
)
assertThat(exception.entityType).isEqualTo("Movie")
assertThat(exception.firstEntityFetcherClass).isEqualTo(EntityFetcherConfig::class.java)

// The order in which the methods are found can vary, so put the methods in a set to assert
val methods = setOf(exception.firstEntityFetcherMethod.name, exception.secondEntityFetcherMethod.name)
assertThat(exception.secondEntityFetcherClass).isEqualTo(EntityFetcherConfig::class.java)
assertThat(methods).contains("movieEntityFetcher", "anotherMovieEntityFetcher")
}
}

@DgsComponent
class EntityFetcherConfig {
@DgsEntityFetcher(name = "Movie")
fun movieEntityFetcher(values: Map<String, Any>): Movie = Movie()

@DgsEntityFetcher(name = "Movie")
fun anotherMovieEntityFetcher(values: Map<String, Any>): Movie = Movie()
}
}

0 comments on commit b4cafa2

Please sign in to comment.