Skip to content

Commit cd4fcbf

Browse files
committed
fix: properly cache GraphQL object/interfaces/union types
graphql-java requires all GraphQL object/interfaces/union types in the graph to have unique names but underlying objects do not implement hashcode and equals. This means that if we are generating schemas based on the reflections we had to ensure that we would only build objects once - we implemented caching logic in the builders. Unfortunately existing logic was putting all objects/interfaces/unions as "under construction" but it only marked objects as completed and only if they were generated from interface builder.
1 parent d31ae62 commit cd4fcbf

File tree

7 files changed

+64
-16
lines changed

7 files changed

+64
-16
lines changed

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/SchemaGenerator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ internal class SchemaGenerator(internal val config: SchemaGeneratorConfig) {
5858
builder.mutation(mutationBuilder.getMutationObject(mutations))
5959
builder.subscription(subscriptionBuilder.getSubscriptionObject(subscriptions))
6060

61-
// add interface/union implementations
62-
state.getValidAdditionalTypes().forEach {
61+
// add interface implementations
62+
state.additionalTypes.forEach {
6363
builder.additionalType(it)
6464
}
6565

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/state/SchemaGeneratorState.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import graphql.schema.GraphQLDirective
66
import graphql.schema.GraphQLType
77
import java.util.concurrent.ConcurrentHashMap
88

9+
@Suppress("UseDataClass")
910
internal class SchemaGeneratorState(supportedPackages: List<String>) {
1011
val cache = TypesCache(supportedPackages)
1112
val additionalTypes = mutableSetOf<GraphQLType>()
1213
val directives = ConcurrentHashMap<String, GraphQLDirective>()
1314

14-
fun getValidAdditionalTypes(): List<GraphQLType> = additionalTypes.filter { cache.doesNotContainGraphQLType(it) }
15-
1615
init {
1716
// NOTE: @include and @defer query directives are added by graphql-java by default
1817
// adding them explicitly here to keep it consistent with missing deprecated directive

graphql-kotlin-schema-generator/src/main/kotlin/com/expedia/graphql/generator/state/TypesCache.kt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import graphql.schema.GraphQLType
1010
import graphql.schema.GraphQLTypeReference
1111
import kotlin.reflect.KClass
1212
import kotlin.reflect.KType
13+
import kotlin.reflect.full.createType
1314
import kotlin.reflect.full.isSubclassOf
1415

1516
internal class TypesCache(private val supportedPackages: List<String>) {
@@ -68,16 +69,19 @@ internal class TypesCache(private val supportedPackages: List<String>) {
6869

6970
private fun isTypeNotSupported(type: KType): Boolean = supportedPackages.none { type.qualifiedName.startsWith(it) }
7071

71-
private fun putTypeUnderConstruction(kClass: KClass<*>) = typeUnderConstruction.add(kClass)
72-
73-
fun removeTypeUnderConstruction(kClass: KClass<*>) = typeUnderConstruction.remove(kClass)
74-
7572
fun buildIfNotUnderConstruction(kClass: KClass<*>, build: (KClass<*>) -> GraphQLType): GraphQLType {
76-
return if (typeUnderConstruction.contains(kClass)) {
77-
GraphQLTypeReference.typeRef(kClass.getSimpleName())
78-
} else {
79-
putTypeUnderConstruction(kClass)
80-
build(kClass)
73+
val cachedType = cache[kClass.getSimpleName()]
74+
return when {
75+
cachedType != null -> cachedType.graphQLType
76+
typeUnderConstruction.contains(kClass) -> GraphQLTypeReference.typeRef(kClass.getSimpleName())
77+
else -> {
78+
typeUnderConstruction.add(kClass)
79+
val newType = build(kClass)
80+
val key = TypesCacheKey(kClass.createType(), false)
81+
put(key, KGraphQLType(kClass, newType))
82+
typeUnderConstruction.remove(kClass)
83+
newType
84+
}
8185
}
8286
}
8387
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,12 @@ internal class InterfaceBuilder(generator: SchemaGenerator) : TypeBuilder(genera
3333

3434
val interfaceType = builder.build()
3535
val implementations = subTypeMapper.getSubTypesOf(kClass)
36-
implementations.forEach {
37-
val objectType = generator.objectType(it.kotlin, interfaceType)
36+
implementations.forEach { implementation ->
37+
val objectType = generator.objectType(implementation.kotlin, interfaceType)
3838

3939
// Only update the state if the object is fully constructed and not a reference
4040
if (objectType !is GraphQLTypeReference) {
4141
state.additionalTypes.add(objectType)
42-
state.cache.removeTypeUnderConstruction(it.kotlin)
4342
}
4443
}
4544

graphql-kotlin-schema-generator/src/test/kotlin/com/expedia/graphql/generator/types/InterfaceBuilderTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import com.expedia.graphql.test.utils.SimpleDirective
66
import graphql.schema.GraphQLInterfaceType
77
import org.junit.jupiter.api.Test
88
import kotlin.test.assertEquals
9+
import kotlin.test.assertFalse
10+
import kotlin.test.assertNotNull
11+
import kotlin.test.assertTrue
912

1013
internal class InterfaceBuilderTest : TypeTestHelper() {
1114

@@ -48,4 +51,17 @@ internal class InterfaceBuilderTest : TypeTestHelper() {
4851
assertEquals(1, result?.directives?.size)
4952
assertEquals("simpleDirective", result?.directives?.first()?.name)
5053
}
54+
55+
@Test
56+
fun `verify interface is build only once`() {
57+
val cache = generator.state.cache
58+
assertTrue(cache.doesNotContain(HappyInterface::class))
59+
60+
val first = builder.interfaceType(HappyInterface::class) as? GraphQLInterfaceType
61+
assertNotNull(first)
62+
assertFalse(cache.doesNotContain(HappyInterface::class))
63+
val second = builder.interfaceType(HappyInterface::class) as? GraphQLInterfaceType
64+
assertNotNull(second)
65+
assertEquals(first.hashCode(), second.hashCode())
66+
}
5167
}

graphql-kotlin-schema-generator/src/test/kotlin/com/expedia/graphql/generator/types/ObjectBuilderTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import graphql.schema.GraphQLNonNull
99
import graphql.schema.GraphQLObjectType
1010
import org.junit.jupiter.api.Test
1111
import kotlin.test.assertEquals
12+
import kotlin.test.assertFalse
1213
import kotlin.test.assertNotNull
14+
import kotlin.test.assertTrue
1315

1416
@Suppress("Detekt.UnusedPrivateClass")
1517
internal class ObjectBuilderTest : TypeTestHelper() {
@@ -67,4 +69,17 @@ internal class ObjectBuilderTest : TypeTestHelper() {
6769
setOf(Introspection.DirectiveLocation.OBJECT)
6870
)
6971
}
72+
73+
@Test
74+
fun `verify object is build only once`() {
75+
val cache = generator.state.cache
76+
assertTrue(cache.doesNotContain(BeHappy::class))
77+
78+
val first = builder.objectType(BeHappy::class) as? GraphQLObjectType
79+
assertNotNull(first)
80+
assertFalse(cache.doesNotContain(BeHappy::class))
81+
val second = builder.objectType(BeHappy::class) as? GraphQLObjectType
82+
assertNotNull(second)
83+
assertEquals(first.hashCode(), second.hashCode())
84+
}
7085
}

graphql-kotlin-schema-generator/src/test/kotlin/com/expedia/graphql/generator/types/UnionBuilderTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import graphql.schema.GraphQLObjectType
77
import graphql.schema.GraphQLUnionType
88
import org.junit.jupiter.api.Test
99
import kotlin.test.assertEquals
10+
import kotlin.test.assertFalse
1011
import kotlin.test.assertNotNull
12+
import kotlin.test.assertTrue
1113

1214
@Suppress("Detekt.UnusedPrivateClass")
1315
internal class UnionBuilderTest : TypeTestHelper() {
@@ -69,4 +71,17 @@ internal class UnionBuilderTest : TypeTestHelper() {
6971
assertEquals(1, result.directives.size)
7072
assertEquals("simpleDirective", result.directives.first().name)
7173
}
74+
75+
@Test
76+
fun `verify union is build only once`() {
77+
val cache = generator.state.cache
78+
assertTrue(cache.doesNotContain(Cake::class))
79+
80+
val first = builder.unionType(Cake::class) as? GraphQLUnionType
81+
assertNotNull(first)
82+
assertFalse(cache.doesNotContain(Cake::class))
83+
val second = builder.unionType(Cake::class) as? GraphQLUnionType
84+
assertNotNull(second)
85+
assertEquals(first.hashCode(), second.hashCode())
86+
}
7287
}

0 commit comments

Comments
 (0)