Skip to content

Commit eb1e9ed

Browse files
committed
TypeDefinitionFactory for dynamically adding type definitions
1 parent 025a2bc commit eb1e9ed

File tree

9 files changed

+166
-31
lines changed

9 files changed

+166
-31
lines changed

src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import graphql.language.FieldDefinition
88
import graphql.language.InputObjectTypeDefinition
99
import graphql.language.InputValueDefinition
1010
import graphql.language.InterfaceTypeDefinition
11+
import graphql.language.ListType
12+
import graphql.language.NonNullType
1113
import graphql.language.ObjectTypeDefinition
1214
import graphql.language.ObjectTypeExtensionDefinition
1315
import graphql.language.ScalarTypeDefinition
@@ -17,7 +19,6 @@ import graphql.language.TypeName
1719
import graphql.language.UnionTypeDefinition
1820
import graphql.schema.GraphQLScalarType
1921
import graphql.schema.idl.ScalarInfo
20-
import graphql.schema.idl.SchemaDirectiveWiring
2122
import org.slf4j.LoggerFactory
2223
import java.lang.reflect.Method
2324

@@ -230,33 +231,51 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
230231
*/
231232
private fun scanQueueItemForPotentialMatches(item: QueueItem) {
232233
val resolverInfoList = this.resolverInfos.filter { it.dataClassType == item.clazz }
233-
val resolverInfo: ResolverInfo? = if (resolverInfoList.size > 1) {
234+
val resolverInfo: ResolverInfo = (if (resolverInfoList.size > 1) {
234235
MultiResolverInfo(resolverInfoList)
235236
} else {
236237
if (item.clazz.equals(Object::class.java)) {
237238
getResolverInfoFromTypeDictionary(item.type.name)
238239
} else {
239240
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
240241
}
241-
}
242-
if (resolverInfo == null) {
243-
throw throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")
244-
}
242+
}) ?: throw throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")
245243

246244
scanResolverInfoForPotentialMatches(item.type, resolverInfo)
247245
}
248246

249247
private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) {
250248
type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field ->
249+
// val searchField = applyDirective(field)
251250
val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)
252251

253252
fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver
253+
254254
fieldResolver.scanForMatches().forEach { potentialMatch ->
255-
handleFoundType(typeClassMatcher.match(potentialMatch))
255+
// if (potentialMatch.graphQLType is TypeName && !definitionsByName.containsKey((potentialMatch.graphQLType.name))) {
256+
// val typeDefinition = ObjectTypeDefinition.newObjectTypeDefinition()
257+
// .name(potentialMatch.graphQLType.name)
258+
// .build()
259+
// handleFoundType(TypeClassMatcher.ValidMatch(typeDefinition, typeClassMatcher.toRealType(potentialMatch), potentialMatch.reference))
260+
// } else {
261+
handleFoundType(typeClassMatcher.match(potentialMatch))
262+
// }
256263
}
257264
}
258265
}
259266

267+
// private fun applyDirective(field: FieldDefinition): FieldDefinition {
268+
// val connectionDirectives = field.directives.filter { it.name == "connection" }
269+
// if (connectionDirectives.isNotEmpty()) {
270+
// val directive = connectionDirectives.first()
271+
// val originalType:TypeName = field.type as TypeName
272+
// val wrappedField = field.deepCopy()
273+
// wrappedField.type = TypeName(originalType.name + "Connection")
274+
// return wrappedField
275+
// }
276+
// return field
277+
// }
278+
260279
private fun handleFoundType(match: TypeClassMatcher.Match) {
261280
when (match) {
262281
is TypeClassMatcher.ScalarMatch -> {

src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
146146
fieldDefinition.description
147147
builder.field { field ->
148148
createField(field, fieldDefinition)
149-
// todo: apply directives to the fieldDefinition to use to find dataFetcher we're really after
150149
field.dataFetcher(fieldResolversByType[definition]?.get(fieldDefinition)?.createDataFetcher() ?: throw SchemaError("No resolver method found for object type '${definition.name}' and field '${fieldDefinition.name}', this is most likely a bug with graphql-java-tools"))
151150

152151
val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring))

src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package com.coxautodev.graphql.tools
22

3+
import com.coxautodev.graphql.tools.relay.RelayConnectionFactory
34
import com.fasterxml.jackson.databind.ObjectMapper
45
import com.google.common.collect.BiMap
56
import com.google.common.collect.HashBiMap
67
import com.google.common.collect.Maps
8+
import graphql.language.Definition
79
import graphql.language.Document
10+
import graphql.language.FieldDefinition
11+
import graphql.language.ListType
12+
import graphql.language.NonNullType
13+
import graphql.language.ObjectTypeDefinition
14+
import graphql.language.TypeName
815
import graphql.parser.Parser
916
import graphql.schema.DataFetchingEnvironment
1017
import graphql.schema.GraphQLScalarType
@@ -149,7 +156,7 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio
149156
* Scan for classes with the supplied schema and dictionary. Used for testing.
150157
*/
151158
private fun scan(): ScannedSchemaObjects {
152-
val definitions = parseDefinitions()
159+
val definitions = appendDynamicDefinitions(parseDefinitions())
153160
val customScalars = scalars.associateBy { it.name }
154161

155162
return SchemaClassScanner(dictionary.getDictionary(), definitions, resolvers, customScalars, options)
@@ -158,6 +165,12 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio
158165

159166
private fun parseDefinitions() = parseDocuments().flatMap { it.definitions }
160167

168+
private fun appendDynamicDefinitions(baseDefinitions: List<Definition<*>>): List<Definition<*>> {
169+
val definitions = baseDefinitions.toMutableList()
170+
options.typeDefinitionFactories.forEach { definitions.addAll(it.create(baseDefinitions)) }
171+
return definitions.toList()
172+
}
173+
161174
private fun parseDocuments(): List<Document> {
162175
val parser = Parser()
163176
val documents = mutableListOf<Document>()
@@ -267,7 +280,8 @@ data class SchemaParserOptions internal constructor(
267280
val proxyHandlers: List<ProxyHandler>,
268281
val preferGraphQLResolver: Boolean,
269282
val introspectionEnabled: Boolean,
270-
val coroutineContext: CoroutineContext
283+
val coroutineContext: CoroutineContext,
284+
val typeDefinitionFactories: List<TypeDefinitionFactory>
271285
) {
272286
companion object {
273287
@JvmStatic
@@ -287,6 +301,7 @@ data class SchemaParserOptions internal constructor(
287301
private var preferGraphQLResolver = false
288302
private var introspectionEnabled = true
289303
private var coroutineContext: CoroutineContext? = null
304+
private var typeDefinitionFactories: MutableList<TypeDefinitionFactory> = mutableListOf(RelayConnectionFactory())
290305

291306
fun contextClass(contextClass: Class<*>) = this.apply {
292307
this.contextClass = contextClass
@@ -340,6 +355,10 @@ data class SchemaParserOptions internal constructor(
340355
this.coroutineContext = context
341356
}
342357

358+
fun typeDefinitionFactory(factory: TypeDefinitionFactory) = this.apply {
359+
this.typeDefinitionFactories.add(factory)
360+
}
361+
343362
fun build(): SchemaParserOptions {
344363
val coroutineContext = coroutineContext ?: Dispatchers.Default
345364
val wrappers = if (useDefaultGenericWrappers) {
@@ -365,7 +384,7 @@ data class SchemaParserOptions internal constructor(
365384
}
366385

367386
return SchemaParserOptions(contextClass, wrappers, allowUnimplementedResolvers, objectMapperProvider,
368-
proxyHandlers, preferGraphQLResolver, introspectionEnabled, coroutineContext)
387+
proxyHandlers, preferGraphQLResolver, introspectionEnabled, coroutineContext, typeDefinitionFactories)
369388
}
370389
}
371390

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.coxautodev.graphql.tools;
2+
3+
import graphql.language.Definition;
4+
5+
import java.util.List;
6+
7+
public interface TypeDefinitionFactory {
8+
9+
List<Definition<?>> create(final List<Definition<?>> existing);
10+
11+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.coxautodev.graphql.tools.relay
2+
3+
import com.coxautodev.graphql.tools.TypeDefinitionFactory
4+
import graphql.language.Definition
5+
import graphql.language.Directive
6+
import graphql.language.FieldDefinition
7+
import graphql.language.ListType
8+
import graphql.language.NonNullType
9+
import graphql.language.ObjectTypeDefinition
10+
import graphql.language.StringValue
11+
import graphql.language.TypeDefinition
12+
import graphql.language.TypeName
13+
14+
class RelayConnectionFactory : TypeDefinitionFactory {
15+
16+
override fun create(existing: List<Definition<*>>): List<Definition<*>> {
17+
val definitions = mutableListOf<Definition<*>>()
18+
val definitionsByName = existing.filterIsInstance<TypeDefinition<*>>()
19+
.associateBy { it.name }
20+
.toMutableMap()
21+
22+
findConnectionDirectives(existing)
23+
.flatMap { createDefinitions(it) }
24+
.forEach {
25+
if (!definitionsByName.containsKey(it.name)) {
26+
definitionsByName[it.name] = it
27+
definitions.add(it)
28+
}
29+
}
30+
31+
if (!definitionsByName.containsKey("PageInfo")) {
32+
definitions.add(createPageInfo())
33+
}
34+
35+
return definitions
36+
}
37+
38+
private fun findConnectionDirectives(definitions: List<Definition<*>>): List<Directive> {
39+
return definitions.filterIsInstance<ObjectTypeDefinition>()
40+
.flatMap { it.fieldDefinitions }
41+
.flatMap { it.directives }
42+
.filter { it.name == "connection" }
43+
}
44+
45+
private fun createDefinitions(directive: Directive): List<ObjectTypeDefinition> {
46+
val definitions = mutableListOf<ObjectTypeDefinition>()
47+
definitions.add(createEdgeDefinition(directive.forTypeName()))
48+
definitions.add(createConnectionDefinition(directive.forTypeName()))
49+
return definitions.toList()
50+
}
51+
52+
private fun createConnectionDefinition(type: String): ObjectTypeDefinition =
53+
ObjectTypeDefinition.newObjectTypeDefinition()
54+
.name(type + "Connection")
55+
.fieldDefinition(FieldDefinition("edges", ListType(TypeName(type + "Edge"))))
56+
.fieldDefinition(FieldDefinition("pageInfo", TypeName("PageInfo")))
57+
.build()
58+
59+
private fun createEdgeDefinition(type: String): ObjectTypeDefinition =
60+
ObjectTypeDefinition.newObjectTypeDefinition()
61+
.name(type + "Edge")
62+
.fieldDefinition(FieldDefinition("cursor", TypeName("String")))
63+
.fieldDefinition(FieldDefinition("node", TypeName(type)))
64+
.build()
65+
66+
private fun createPageInfo(): ObjectTypeDefinition =
67+
ObjectTypeDefinition.newObjectTypeDefinition()
68+
.name("PageInfo")
69+
.fieldDefinition(FieldDefinition("hasPreviousPage", NonNullType(TypeName("Boolean"))))
70+
.fieldDefinition(FieldDefinition("hasNextPage", NonNullType(TypeName("Boolean"))))
71+
.build()
72+
73+
private fun Directive.forTypeName(): String {
74+
return (this.getArgument("for").value as StringValue).value
75+
}
76+
77+
}

src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ class DirectiveBehavior {
1515
return directiveHelper.onField(element, params.toParameters())
1616
}
1717

18-
1918
data class Params(val runtimeWiring: RuntimeWiring) {
2019
internal fun toParameters() = SchemaGeneratorDirectiveHelper.Parameters(null, runtimeWiring, null, null)
2120
}
21+
2222
}

src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,30 @@
44
import graphql.execution.AsyncExecutionStrategy;
55
import graphql.schema.GraphQLSchema;
66
import groovy.lang.Closure;
7+
import io.reactivex.Single;
8+
import io.reactivex.internal.operators.single.SingleJust;
79
import org.junit.Test;
810

911
import java.util.HashMap;
12+
import java.util.Optional;
13+
import java.util.concurrent.CompletableFuture;
1014
import java.util.concurrent.Future;
1115

16+
import static io.reactivex.Maybe.just;
17+
1218
public class ReactiveTest {
1319

1420
@Test
1521
public void futureSucceeds() {
22+
SchemaParserOptions options = SchemaParserOptions.newOptions()
23+
.genericWrappers(
24+
new SchemaParserOptions.GenericWrapper(Single.class, 0),
25+
new SchemaParserOptions.GenericWrapper(SingleJust.class, 0)
26+
)
27+
.build();
1628
GraphQLSchema schema = SchemaParser.newParser().file("Reactive.graphqls")
1729
.resolvers(new Query())
30+
.options(options)
1831
.build()
1932
.makeExecutableSchema();
2033

@@ -30,8 +43,12 @@ public String call() {
3043
}
3144

3245
static class Query implements GraphQLQueryResolver {
33-
Future<Organization> organization(int organizationid) {
34-
return null;
46+
// Single<Optional<Organization>> organization(int organizationid) {
47+
// return Single.just(Optional.empty()); //CompletableFuture.completedFuture(null);
48+
// }
49+
50+
Future<Optional<Organization>> organization(int organizationid) {
51+
return CompletableFuture.completedFuture(Optional.of(new Organization()));
3552
}
3653
}
3754

src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ public class RelayConnectionTest {
2020

2121
@Test
2222
public void compiles() {
23+
SchemaParserOptions options = SchemaParserOptions.newOptions()
24+
25+
.build();
2326
GraphQLSchema schema = SchemaParser.newParser().file("RelayConnection.graphqls")
2427
.resolvers(new QueryResolver())
2528
.dictionary(User.class)
2629
.directive("connection", new ConnectionDirective())
30+
.options(options)
2731
.build()
2832
.makeExecutableSchema();
2933

@@ -39,10 +43,15 @@ public String call() {
3943
return "query {\n" +
4044
" users {\n" +
4145
" edges {\n" +
46+
" cursor\n" +
4247
" node {\n" +
4348
" id\n" +
4449
" name\n" +
4550
" }\n" +
51+
" },\n" +
52+
" pageInfo {\n" +
53+
" hasPreviousPage,\n" +
54+
" hasNextPage\n" +
4655
" }\n" +
4756
" }\n" +
4857
"}";
Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
1-
directive @connection on FIELD_DEFINITION
2-
31
type Query {
4-
users(first: Int, after: String): UserConnection @connection
5-
}
6-
7-
type UserConnection {
8-
edges: [UserEdge!]!
9-
pageInfo: PageInfo!
10-
}
11-
12-
type UserEdge {
13-
cursor: String!
14-
node: User!
2+
users(first: Int, after: String): UserConnection @connection(for: "User")
153
}
164

175
type User {
186
id: ID!
197
name: String
208
}
21-
22-
type PageInfo {
23-
24-
}

0 commit comments

Comments
 (0)