├── .gitignore ├── .idea ├── compiler.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── java │ └── com │ │ └── coxautodev │ │ └── graphql │ │ └── tools │ │ ├── GraphQLMutationResolver.java │ │ ├── GraphQLQueryResolver.java │ │ ├── GraphQLResolver.java │ │ ├── GraphQLSubscriptionResolver.java │ │ ├── GuiceAopProxyHandler.java │ │ ├── ObjectMapperConfigurer.java │ │ └── ProxyHandler.java ├── kotlin │ └── com │ │ ├── coxautodev │ │ └── graphql │ │ │ └── tools │ │ │ ├── DictionaryTypeResolver.kt │ │ │ ├── FieldResolver.kt │ │ │ ├── FieldResolverScanner.kt │ │ │ ├── GenericType.kt │ │ │ ├── MethodFieldResolver.kt │ │ │ ├── ObjectMapperConfigurerContext.kt │ │ │ ├── PropertyFieldResolver.kt │ │ │ ├── ResolverInfo.kt │ │ │ ├── RootTypeInfo.kt │ │ │ ├── ScannedSchemaObjects.kt │ │ │ ├── SchemaClassScanner.kt │ │ │ ├── SchemaObjects.kt │ │ │ ├── SchemaParser.kt │ │ │ ├── SchemaParserBuilder.kt │ │ │ ├── Spring4AopProxyHandler.kt │ │ │ ├── TypeClassMatcher.kt │ │ │ └── Utils.kt │ │ ├── framework │ │ ├── Application.kt │ │ ├── datamodel │ │ │ ├── edges │ │ │ │ ├── Linked.kt │ │ │ │ ├── Relationship.kt │ │ │ │ └── Traversal.kt │ │ │ └── node │ │ │ │ ├── Node.kt │ │ │ │ ├── NodeQueryResolver.kt │ │ │ │ └── NodeTypeResolver.kt │ │ ├── graphql │ │ │ ├── Mutation.kt │ │ │ └── TraversalDataLoader.kt │ │ └── gremlin │ │ │ └── Extensions.kt │ │ └── starwars │ │ ├── StarwarsGraph.kt │ │ ├── character │ │ ├── Character.kt │ │ ├── CharacterQueryResolver.kt │ │ └── CharacterTypeResolver.kt │ │ ├── droid │ │ ├── Droid.kt │ │ ├── DroidMutationResolver.kt │ │ ├── DroidQueryResolver.kt │ │ └── DroidTypeResolver.kt │ │ ├── episode │ │ ├── Episode.kt │ │ └── EpisodeTypeResolver.kt │ │ └── human │ │ ├── Human.kt │ │ ├── HumanQueryResolver.kt │ │ └── HumanTypeResolver.kt └── resources │ ├── application.properties │ ├── graphqls │ └── starwars.graphqls │ ├── janusgraph-configuration.properties │ └── static │ └── graphiql.html └── test └── kotlin └── com └── starwars └── GraphTests.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | /out/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/**/libraries 8 | .idea/**/workspace.xml 9 | *.iws 10 | *.iml 11 | *.ipr 12 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An Example GraphQL + JanusGraph Web App 2 | 3 | This repository demonstrates how to implement a graph-based web application using JanusGraph. It is written in Kotlin and exposes a GraphQL interface. 4 | 5 | The application was written using: 6 | 7 | - [Kotlin](https://kotlinlang.org/) 8 | - [Janus Graph](http://janusgraph.org/) 9 | - [Ferma OGM](http://syncleus.com/Ferma/) 10 | - [Spring Boot](https://projects.spring.io/spring-boot/) 11 | - [GraphQL](http://graphql.org/) 12 | - [GraphQL Tools](https://github.com/graphql-java/graphql-java-tools) 13 | 14 | The project is split into 2 packages, `framework` and `starwars`. `framework` adds some features to the Ferma OGM, such as providing type-safe traversals. The `starwars` package shows how any data model could plug into the `framework` package and Ferma OGM with minimal effort. The `framework` package is not currently offered as a library due to its lack of documentation and testing. 15 | 16 | 17 | To build and run the application locally, from the repository root: 18 | ``` 19 | gradlew run 20 | ``` 21 | 22 | Then load `http://localhost:5000/graphiql.html` and start playing around in GraphiQL 23 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.2.21' 4 | springBootVersion = '1.5.10.BUILD-SNAPSHOT' 5 | } 6 | repositories { 7 | mavenCentral() 8 | maven { url "https://repo.spring.io/snapshot" } 9 | maven { url "https://repo.spring.io/milestone" } 10 | } 11 | dependencies { 12 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 13 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 14 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 15 | } 16 | } 17 | 18 | apply plugin: 'kotlin' 19 | apply plugin: 'kotlin-spring' 20 | apply plugin: 'org.springframework.boot' 21 | apply plugin: 'io.spring.dependency-management' 22 | apply plugin: 'application' 23 | apply plugin: 'idea' 24 | 25 | repositories { 26 | mavenCentral() 27 | maven { url "https://repo.spring.io/snapshot" } 28 | maven { url "https://repo.spring.io/milestone" } 29 | } 30 | 31 | mainClassName = 'com.framework.ApplicationKt' 32 | 33 | kotlin { experimental { coroutines 'enable' } } 34 | 35 | dependencies { 36 | 37 | compile("org.springframework.boot:spring-boot-starter-actuator") 38 | compile("org.springframework.boot:spring-boot-starter-web") 39 | compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}") 40 | compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") 41 | compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.3") 42 | compile("com.graphql-java:graphql-spring-boot-starter:3.10.0") 43 | compile("org.janusgraph:janusgraph-core:0.2.0") 44 | compile("com.syncleus.ferma:ferma:3.2.1") 45 | compile("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:0.21.2") 46 | 47 | testCompile('org.springframework.boot:spring-boot-starter-test') 48 | 49 | 50 | // Graphql tools dependency 51 | compile("com.esotericsoftware:reflectasm:1.11.3") 52 | } 53 | 54 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 55 | sourceCompatibility = JavaVersion.VERSION_1_8 56 | targetCompatibility = JavaVersion.VERSION_1_8 57 | kotlinOptions { 58 | jvmTarget = "1.8" 59 | apiVersion = "1.1" 60 | languageVersion = "1.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pm-dev/janusgraph-exploration/638b34af9ac67ed28c1acdff468a7b978f678079/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 17 20:58:48 PST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/coxautodev/graphql/tools/GraphQLMutationResolver.java: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools; 2 | 3 | /** 4 | * @author Andrew Potter 5 | */ 6 | public interface GraphQLMutationResolver extends GraphQLResolver { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/coxautodev/graphql/tools/GraphQLQueryResolver.java: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools; 2 | 3 | /** 4 | * @author Andrew Potter 5 | */ 6 | public interface GraphQLQueryResolver extends GraphQLResolver { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/coxautodev/graphql/tools/GraphQLResolver.java: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools; 2 | 3 | /** 4 | * @author Andrew Potter 5 | */ 6 | public interface GraphQLResolver { 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/com/coxautodev/graphql/tools/GraphQLSubscriptionResolver.java: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools; 2 | 3 | public interface GraphQLSubscriptionResolver extends GraphQLResolver { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/coxautodev/graphql/tools/GuiceAopProxyHandler.java: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools; 2 | 3 | public class GuiceAopProxyHandler implements ProxyHandler { 4 | @Override 5 | public boolean canHandle(GraphQLResolver resolver) { 6 | return isGuiceProxy(resolver); 7 | } 8 | 9 | @Override 10 | public Class getTargetClass(GraphQLResolver resolver) { 11 | return resolver.getClass().getSuperclass(); 12 | } 13 | 14 | private boolean isGuiceProxy(GraphQLResolver resolver) { 15 | return resolver.getClass().getName().contains("$$EnhancerByGuice$$"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/coxautodev/graphql/tools/ObjectMapperConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | /** 6 | * @author Andrew Potter 7 | */ 8 | public interface ObjectMapperConfigurer { 9 | void configure(ObjectMapper mapper, ObjectMapperConfigurerContext context); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/coxautodev/graphql/tools/ProxyHandler.java: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools; 2 | 3 | /** 4 | * @author Andrew Potter 5 | */ 6 | public interface ProxyHandler { 7 | boolean canHandle(GraphQLResolver resolver); 8 | Class getTargetClass(GraphQLResolver resolver); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/DictionaryTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import com.google.common.collect.BiMap 4 | import graphql.TypeResolutionEnvironment 5 | import graphql.language.TypeDefinition 6 | import graphql.schema.GraphQLInterfaceType 7 | import graphql.schema.GraphQLObjectType 8 | import graphql.schema.GraphQLUnionType 9 | import graphql.schema.TypeResolver 10 | 11 | /** 12 | * @author Andrew Potter 13 | */ 14 | abstract class DictionaryTypeResolver(private val dictionary: BiMap, TypeDefinition>, private val types: Map) : TypeResolver { 15 | 16 | private val namesToClass = dictionary.inverse().mapKeys { it.key.name } 17 | 18 | override fun getType(env: TypeResolutionEnvironment): GraphQLObjectType? { 19 | val obj = env.getObject() 20 | val clazz = obj.javaClass 21 | val name = dictionary[clazz]?.name ?: clazz.simpleName 22 | val type = types[name] 23 | if (type != null) { 24 | return type 25 | } 26 | for (validType in types) { 27 | if (namesToClass[validType.key]?.isInstance(obj) == true) { 28 | return validType.value 29 | } 30 | } 31 | throw TypeResolverError(getError(name)) 32 | } 33 | 34 | abstract fun getError(name: String): String 35 | } 36 | 37 | class InterfaceTypeResolver(dictionary: BiMap, TypeDefinition>, private val thisInterface: GraphQLInterfaceType, types: List) : DictionaryTypeResolver(dictionary, types.filter { it.interfaces.any { it.name == thisInterface.name } }.associateBy { it.name }) { 38 | override fun getError(name: String) = "Expected object type with name '$name' to implement interface '${thisInterface.name}', but it doesn't!" 39 | } 40 | 41 | class UnionTypeResolver(dictionary: BiMap, TypeDefinition>, private val thisUnion: GraphQLUnionType, types: List) : DictionaryTypeResolver(dictionary, types.filter { type -> thisUnion.types.any { it.name == type.name } }.associateBy { it.name }) { 42 | override fun getError(name: String) = "Expected object type with name '$name' to exist for union '${thisUnion.name}', but it doesn't!" 43 | } 44 | 45 | class TypeResolverError(message: String, cause: Throwable? = null) : RuntimeException(message, cause) 46 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/FieldResolver.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.language.FieldDefinition 4 | import graphql.schema.DataFetcher 5 | import graphql.schema.DataFetchingEnvironment 6 | 7 | /** 8 | * @author Andrew Potter 9 | */ 10 | internal abstract class FieldResolver(val field: FieldDefinition, val search: FieldResolverScanner.Search, val options: SchemaParserOptions, relativeTo: JavaType) { 11 | val resolverInfo: ResolverInfo = search.resolverInfo 12 | val genericType = GenericType(search.type, options).relativeToPotentialParent(relativeTo) 13 | 14 | abstract fun scanForMatches(): List 15 | abstract fun createDataFetcher(): DataFetcher<*> 16 | 17 | /** 18 | * Add source resolver depending on whether or not this is a resolver method 19 | */ 20 | protected fun getSourceResolver(): SourceResolver = if(this.search.source != null) { 21 | ({ this.search.source }) 22 | } else { 23 | ({ environment -> 24 | val source = environment.getSource() 25 | 26 | if(!this.genericType.isAssignableFrom(source.javaClass)) { 27 | throw ResolverError("Expected source object to be an instance of '${this.genericType.getRawClass().name}' but instead got '${source.javaClass.name}'") 28 | } 29 | 30 | source 31 | }) 32 | } 33 | } 34 | 35 | internal class MissingFieldResolver(field: FieldDefinition, options: SchemaParserOptions): FieldResolver(field, FieldResolverScanner.Search(Any::class.java, MissingResolverInfo(), null), options, Any::class.java) { 36 | override fun scanForMatches(): List = listOf() 37 | override fun createDataFetcher(): DataFetcher<*> = DataFetcher { TODO("Schema resolver not implemented") } 38 | } 39 | 40 | internal typealias SourceResolver = (DataFetchingEnvironment) -> Any 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/FieldResolverScanner.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.Scalars 4 | import graphql.language.FieldDefinition 5 | import graphql.language.TypeName 6 | import graphql.schema.DataFetchingEnvironment 7 | import org.apache.commons.lang3.ClassUtils 8 | import org.apache.commons.lang3.reflect.FieldUtils 9 | import org.slf4j.LoggerFactory 10 | import java.lang.reflect.Modifier 11 | import java.lang.reflect.ParameterizedType 12 | 13 | /** 14 | * @author Andrew Potter 15 | */ 16 | internal class FieldResolverScanner(val options: SchemaParserOptions) { 17 | 18 | companion object { 19 | private val log = LoggerFactory.getLogger(FieldResolverScanner::class.java) 20 | 21 | fun getAllMethods(type: Class<*>) = 22 | type.declaredMethods.toList() + ClassUtils.getAllSuperclasses(type).flatMap { it.declaredMethods.toList() }.filter { !Modifier.isPrivate(it.modifiers) } 23 | } 24 | 25 | fun findFieldResolver(field: FieldDefinition, resolverInfo: ResolverInfo): FieldResolver { 26 | val searches = resolverInfo.getFieldSearches() 27 | 28 | val scanProperties = field.inputValueDefinitions.isEmpty() 29 | val found = searches.mapNotNull { search -> findFieldResolver(field, search, scanProperties) } 30 | 31 | if (resolverInfo is RootResolverInfo && found.size > 1) { 32 | throw FieldResolverError("Found more than one matching resolver for field '$field': $found") 33 | } 34 | 35 | return found.firstOrNull() ?: missingFieldResolver(field, searches, scanProperties) 36 | } 37 | 38 | private fun missingFieldResolver(field: FieldDefinition, searches: List, scanProperties: Boolean): FieldResolver { 39 | return if (options.allowUnimplementedResolvers) { 40 | log.warn("Missing resolver for field: $field") 41 | 42 | MissingFieldResolver(field, options) 43 | } else { 44 | throw FieldResolverError(getMissingFieldMessage(field, searches, scanProperties)) 45 | } 46 | } 47 | 48 | private fun findFieldResolver(field: FieldDefinition, search: Search, scanProperties: Boolean): FieldResolver? { 49 | val method = findResolverMethod(field, search) 50 | if (method != null) { 51 | return MethodFieldResolver(field, search, options, method.apply { isAccessible = true }) 52 | } 53 | 54 | if (scanProperties) { 55 | val property = findResolverProperty(field, search) 56 | if (property != null) { 57 | return PropertyFieldResolver(field, search, options, property.apply { isAccessible = true }) 58 | } 59 | } 60 | 61 | return null 62 | } 63 | 64 | private fun isBoolean(type: GraphQLLangType) = type.unwrap().let { it is TypeName && it.name == Scalars.GraphQLBoolean.name } 65 | 66 | private fun findResolverMethod(field: FieldDefinition, search: Search): java.lang.reflect.Method? { 67 | 68 | val methods = getAllMethods(search.type) 69 | val argumentCount = field.inputValueDefinitions.size + if (search.requiredFirstParameterType != null) 1 else 0 70 | val name = field.name 71 | 72 | val isBoolean = isBoolean(field.type) 73 | 74 | // Check for the following one by one: 75 | // 1. Method with exact field name 76 | // 2. Method that returns a boolean with "is" style getter 77 | // 3. Method with "get" style getter 78 | return methods.find { 79 | it.name == name && verifyMethodArguments(it, argumentCount, search) 80 | } ?: methods.find { 81 | (isBoolean && it.name == "is${name.capitalize()}") && verifyMethodArguments(it, argumentCount, search) 82 | } ?: methods.find { 83 | it.name == "get${name.capitalize()}" && verifyMethodArguments(it, argumentCount, search) 84 | } 85 | } 86 | 87 | private fun verifyMethodArguments(method: java.lang.reflect.Method, requiredCount: Int, search: Search): Boolean { 88 | val appropriateFirstParameter = if (search.requiredFirstParameterType != null) { 89 | if(MethodFieldResolver.isBatched(method, search)) { 90 | verifyBatchedMethodFirstArgument(method.genericParameterTypes.firstOrNull(), search.requiredFirstParameterType) 91 | } else { 92 | method.parameterTypes.firstOrNull() == search.requiredFirstParameterType 93 | } 94 | } else { 95 | true 96 | } 97 | 98 | val correctParameterCount = method.parameterCount == requiredCount || (method.parameterCount == (requiredCount + 1) && method.parameterTypes.last() == DataFetchingEnvironment::class.java) 99 | return correctParameterCount && appropriateFirstParameter 100 | } 101 | 102 | private fun verifyBatchedMethodFirstArgument(firstType: JavaType?, requiredFirstParameterType: Class<*>?): Boolean { 103 | if(firstType == null) { 104 | return false 105 | } 106 | 107 | if(firstType !is ParameterizedType) { 108 | return false 109 | } 110 | 111 | if(!TypeClassMatcher.isListType(firstType, GenericType(firstType, options))) { 112 | return false 113 | } 114 | 115 | val typeArgument = firstType.actualTypeArguments.first() as? Class<*> ?: return false 116 | 117 | return typeArgument == requiredFirstParameterType 118 | } 119 | 120 | private fun findResolverProperty(field: FieldDefinition, search: Search) = 121 | FieldUtils.getAllFields(search.type).find { it.name == field.name } 122 | 123 | private fun getMissingFieldMessage(field: FieldDefinition, searches: List, scannedProperties: Boolean): String { 124 | val signatures = mutableListOf("") 125 | val isBoolean = isBoolean(field.type) 126 | 127 | searches.forEach { search -> 128 | signatures.addAll(getMissingMethodSignatures(field, search, isBoolean, scannedProperties)) 129 | } 130 | 131 | return "No method${if (scannedProperties) " or field" else ""} found with any of the following signatures (with or without ${DataFetchingEnvironment::class.java.name} as the last argument), in priority order:\n${signatures.joinToString("\n ")}" 132 | } 133 | 134 | private fun getMissingMethodSignatures(field: FieldDefinition, search: Search, isBoolean: Boolean, scannedProperties: Boolean): List { 135 | val baseType = search.type 136 | val signatures = mutableListOf() 137 | val args = mutableListOf() 138 | val sep = ", " 139 | 140 | if (search.requiredFirstParameterType != null) { 141 | args.add(search.requiredFirstParameterType.name) 142 | } 143 | 144 | args.addAll(field.inputValueDefinitions.map { "~${it.name}" }) 145 | 146 | val argString = args.joinToString(sep) 147 | 148 | signatures.add("${baseType.name}.${field.name}($argString)") 149 | if (isBoolean) { 150 | signatures.add("${baseType.name}.is${field.name.capitalize()}($argString)") 151 | } 152 | signatures.add("${baseType.name}.get${field.name.capitalize()}($argString)") 153 | if (scannedProperties) { 154 | signatures.add("${baseType.name}.${field.name}") 155 | } 156 | 157 | return signatures 158 | } 159 | 160 | data class Search(val type: Class<*>, val resolverInfo: ResolverInfo, val source: Any?, val requiredFirstParameterType: Class<*>? = null, val allowBatched: Boolean = false) 161 | } 162 | 163 | class FieldResolverError(msg: String): RuntimeException(msg) 164 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/GenericType.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import com.google.common.primitives.Primitives 4 | import org.apache.commons.lang3.reflect.TypeUtils 5 | import sun.reflect.generics.reflectiveObjects.WildcardTypeImpl 6 | import java.lang.reflect.ParameterizedType 7 | import java.lang.reflect.TypeVariable 8 | 9 | /** 10 | * @author Andrew Potter 11 | */ 12 | open internal class GenericType(protected val mostSpecificType: JavaType, protected val options: SchemaParserOptions) { 13 | 14 | fun isTypeAssignableFromRawClass(type: ParameterizedType, clazz: Class<*>) = 15 | clazz.isAssignableFrom(getRawClass(type.rawType)) 16 | 17 | fun getRawClass() = getRawClass(mostSpecificType) 18 | 19 | fun getRawClass(type: JavaType): Class<*> = TypeUtils.getRawType(type, mostSpecificType) 20 | 21 | fun isAssignableFrom(type: JavaType) = TypeUtils.isAssignable(type, mostSpecificType) 22 | 23 | fun relativeToPotentialParent(declaringType: JavaType): RelativeTo { 24 | if(declaringType !is Class<*>) { 25 | return relativeToType(declaringType) 26 | } 27 | 28 | val type = getGenericSuperType(mostSpecificType, declaringType) 29 | if(type == null) { 30 | error("Unable to find generic type of class ${TypeUtils.toString(declaringType)} relative to ${TypeUtils.toString(mostSpecificType)}") 31 | } else { 32 | return relativeToType(type) 33 | } 34 | } 35 | fun relativeToType(declaringType: JavaType) = RelativeTo(declaringType, mostSpecificType, options) 36 | 37 | fun getGenericInterface(targetInterface: Class<*>) = getGenericInterface(mostSpecificType, targetInterface) 38 | 39 | private fun getGenericInterface(type: JavaType?, targetInterface: Class<*>): JavaType? { 40 | if(type == null) { 41 | return null 42 | } 43 | 44 | val raw = type as? Class<*> ?: getRawClass(type) 45 | 46 | if(raw == targetInterface) { 47 | return type 48 | } 49 | 50 | val possibleSubInterface = raw.genericInterfaces.find { genericInterface -> 51 | TypeUtils.isAssignable(genericInterface, targetInterface) 52 | } ?: raw.interfaces.find { iface -> 53 | TypeUtils.isAssignable(iface, targetInterface) 54 | } ?: getGenericInterface(raw.genericSuperclass, targetInterface) ?: return null 55 | 56 | return getGenericInterface(possibleSubInterface, targetInterface) 57 | } 58 | 59 | fun getGenericSuperType(targetSuperClass: Class<*>) = getGenericSuperType(mostSpecificType, targetSuperClass) 60 | 61 | private fun getGenericSuperType(type: JavaType?, targetSuperClass: Class<*>): JavaType? { 62 | if(type == null) { 63 | return null 64 | } 65 | 66 | val raw = type as? Class<*> ?: TypeUtils.getRawType(type, type) 67 | 68 | if(raw == targetSuperClass) { 69 | return type 70 | } 71 | 72 | return getGenericSuperType(raw.genericSuperclass, targetSuperClass) 73 | } 74 | 75 | class RelativeTo(private val declaringType: JavaType, mostSpecificType: JavaType, options: SchemaParserOptions): GenericType(mostSpecificType, options) { 76 | 77 | /** 78 | * Unwrap certain Java types to find the "real" class. 79 | */ 80 | fun unwrapGenericType(type: JavaType): JavaType { 81 | return when(type) { 82 | is ParameterizedType -> { 83 | val rawType = type.rawType 84 | val genericType = options.genericWrappers.find { it.type == rawType } ?: return type 85 | 86 | val typeArguments = type.actualTypeArguments 87 | if(typeArguments.size <= genericType.index) { 88 | throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${genericType.index}!") 89 | } 90 | 91 | return unwrapGenericType(typeArguments[genericType.index]) 92 | } 93 | is Class<*> -> if(type.isPrimitive) Primitives.wrap(type) else type 94 | is TypeVariable<*> -> { 95 | if(declaringType !is ParameterizedType) { 96 | error("Could not resolve type variable '${TypeUtils.toLongString(type)}' because declaring type is not parameterized: ${TypeUtils.toString(declaringType)}") 97 | } 98 | 99 | unwrapGenericType(TypeUtils.determineTypeArguments(getRawClass(mostSpecificType), declaringType)[type] ?: error("No type variable found for: ${TypeUtils.toLongString(type)}")) 100 | } 101 | is WildcardTypeImpl -> type.upperBounds.firstOrNull() ?: throw error("Unable to unwrap type, wildcard has no upper bound: $type") 102 | else -> error("Unable to unwrap type: $type") 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import com.esotericsoftware.reflectasm.MethodAccess 4 | import com.fasterxml.jackson.core.type.TypeReference 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module 7 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule 8 | import graphql.execution.batched.Batched 9 | import graphql.language.FieldDefinition 10 | import graphql.language.NonNullType 11 | import graphql.schema.DataFetcher 12 | import graphql.schema.DataFetchingEnvironment 13 | import java.lang.reflect.Method 14 | import java.util.Optional 15 | 16 | /** 17 | * @author Andrew Potter 18 | */ 19 | internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, val method: Method): FieldResolver(field, search, options, method.declaringClass) { 20 | 21 | companion object { 22 | fun isBatched(method: Method, search: FieldResolverScanner.Search): Boolean { 23 | if(method.getAnnotation(Batched::class.java) != null) { 24 | if(!search.allowBatched) { 25 | throw ResolverError("The @Batched annotation is only allowed on non-root resolver methods, but it was found on ${search.type.name}#${method.name}!") 26 | } 27 | 28 | return true 29 | } 30 | 31 | return false 32 | } 33 | } 34 | 35 | private val dataFetchingEnvironment = method.parameterCount == (field.inputValueDefinitions.size + getIndexOffset() + 1) 36 | 37 | override fun createDataFetcher(): DataFetcher<*> { 38 | val batched = isBatched(method, search) 39 | val args = mutableListOf() 40 | val mapper = ObjectMapper().apply { 41 | options.objectMapperConfigurer.configure(this, ObjectMapperConfigurerContext(field)) 42 | }.registerModule(Jdk8Module()).registerKotlinModule() 43 | 44 | // Add source argument if this is a resolver (but not a root resolver) 45 | if(this.search.requiredFirstParameterType != null) { 46 | val expectedType = if(batched) Iterable::class.java else this.search.requiredFirstParameterType 47 | 48 | args.add({ environment -> 49 | val source = environment.getSource() 50 | if (!(expectedType.isAssignableFrom(source.javaClass))) { 51 | throw ResolverError("Source type (${source.javaClass.name}) is not expected type (${expectedType.name})!") 52 | } 53 | 54 | source 55 | }) 56 | } 57 | 58 | // Add an argument for each argument defined in the GraphQL schema 59 | this.field.inputValueDefinitions.forEachIndexed { index, definition -> 60 | 61 | val genericParameterType = this.getJavaMethodParameterType(index) ?: throw ResolverError("Missing method type at position ${this.getJavaMethodParameterIndex(index)}, this is most likely a bug with graphql-java-tools") 62 | 63 | val isNonNull = definition.type is NonNullType 64 | val isOptional = this.genericType.getRawClass(genericParameterType) == Optional::class.java 65 | 66 | val typeReference = object: TypeReference() { 67 | override fun getType() = genericParameterType 68 | } 69 | 70 | args.add({ environment -> 71 | val value = environment.arguments[definition.name] ?: if(isNonNull) { 72 | throw ResolverError("Missing required argument with name '${definition.name}', this is most likely a bug with graphql-java-tools") 73 | } else { 74 | null 75 | } 76 | 77 | if(value == null && isOptional) { 78 | return@add Optional.empty() 79 | } 80 | 81 | return@add mapper.convertValue(value, typeReference) 82 | }) 83 | } 84 | 85 | // Add DataFetchingEnvironment argument 86 | if(this.dataFetchingEnvironment) { 87 | args.add({ environment -> environment }) 88 | } 89 | 90 | return if(batched) { 91 | BatchedMethodFieldResolverDataFetcher(getSourceResolver(), this.method, args) 92 | } else { 93 | MethodFieldResolverDataFetcher(getSourceResolver(), this.method, args) 94 | } 95 | } 96 | 97 | override fun scanForMatches(): List { 98 | val batched = isBatched(method, search) 99 | val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, method.genericReturnType, genericType, SchemaClassScanner.ReturnValueReference(method), batched) 100 | 101 | return field.inputValueDefinitions.mapIndexed { i, inputDefinition -> 102 | TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getJavaMethodParameterType(i)!!, genericType, SchemaClassScanner.MethodParameterReference(method, i), batched) 103 | } + listOf(returnValueMatch) 104 | } 105 | 106 | private fun getIndexOffset() = if(resolverInfo is NormalResolverInfo) 1 else 0 107 | private fun getJavaMethodParameterIndex(index: Int) = index + getIndexOffset() 108 | 109 | private fun getJavaMethodParameterType(index: Int): JavaType? { 110 | val methodIndex = getJavaMethodParameterIndex(index) 111 | val parameters = method.parameterTypes 112 | 113 | return if(parameters.size > methodIndex) { 114 | method.genericParameterTypes[getJavaMethodParameterIndex(index)] 115 | } else { 116 | null 117 | } 118 | } 119 | 120 | override fun toString() = "MethodFieldResolver{method=$method}" 121 | } 122 | 123 | open class MethodFieldResolverDataFetcher(private val sourceResolver: SourceResolver, method: Method, private val args: List): DataFetcher { 124 | 125 | // Convert to reflactasm reflection 126 | private val methodAccess = MethodAccess.get(method.declaringClass)!! 127 | private val methodIndex = methodAccess.getIndex(method.name, *method.parameterTypes) 128 | 129 | override fun get(environment: DataFetchingEnvironment): Any? { 130 | val source = sourceResolver(environment) 131 | val args = this.args.map { it(environment) }.toTypedArray() 132 | val result = methodAccess.invoke(source, methodIndex, *args) 133 | return if(result != null && result is Optional<*>) result.orElse(null) else result 134 | } 135 | } 136 | 137 | class BatchedMethodFieldResolverDataFetcher(sourceResolver: SourceResolver, method: Method, args: List): MethodFieldResolverDataFetcher(sourceResolver, method, args) { 138 | @Batched override fun get(environment: DataFetchingEnvironment) = super.get(environment) 139 | } 140 | 141 | internal typealias ArgumentPlaceholder = (DataFetchingEnvironment) -> Any? 142 | 143 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/ObjectMapperConfigurerContext.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.language.FieldDefinition 4 | 5 | /** 6 | * @author Andrew Potter 7 | */ 8 | data class ObjectMapperConfigurerContext(val field: FieldDefinition) -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/PropertyFieldResolver.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.language.FieldDefinition 4 | import graphql.schema.DataFetcher 5 | import graphql.schema.DataFetchingEnvironment 6 | import java.lang.reflect.Field 7 | 8 | /** 9 | * @author Andrew Potter 10 | */ 11 | internal class PropertyFieldResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, val property: Field): FieldResolver(field, search, options, property.declaringClass) { 12 | override fun createDataFetcher(): DataFetcher<*> { 13 | return PropertyFieldResolverDataFetcher(getSourceResolver(), property) 14 | } 15 | 16 | override fun scanForMatches(): List { 17 | return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, property.genericType, genericType, SchemaClassScanner.FieldTypeReference(property), false)) 18 | } 19 | 20 | override fun toString() = "PropertyFieldResolver{property=$property}" 21 | } 22 | 23 | class PropertyFieldResolverDataFetcher(private val sourceResolver: SourceResolver, private val field: Field): DataFetcher { 24 | override fun get(environment: DataFetchingEnvironment): Any? { 25 | return field.get(sourceResolver(environment)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/ResolverInfo.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import org.apache.commons.lang3.reflect.TypeUtils 4 | import java.lang.reflect.ParameterizedType 5 | 6 | internal abstract class ResolverInfo { 7 | abstract fun getFieldSearches(): List 8 | 9 | fun getRealResolverClass(resolver: GraphQLResolver<*>, options: SchemaParserOptions) = 10 | options.proxyHandlers.find { it.canHandle(resolver) }?.getTargetClass(resolver) ?: resolver.javaClass 11 | } 12 | 13 | internal class NormalResolverInfo(val resolver: GraphQLResolver<*>, private val options: SchemaParserOptions): ResolverInfo() { 14 | private val resolverType = getRealResolverClass(resolver, options) 15 | val dataClassType = findDataClass() 16 | 17 | private fun findDataClass(): Class<*> { 18 | // Grab the parent interface with type GraphQLResolver from our resolver and get its first type argument. 19 | val interfaceType = GenericType(resolverType, options).getGenericInterface(GraphQLResolver::class.java) 20 | if(interfaceType == null || interfaceType !is ParameterizedType) { 21 | error("${GraphQLResolver::class.java.simpleName} interface was not parameterized for: ${resolverType.name}") 22 | } 23 | 24 | val type = TypeUtils.determineTypeArguments(resolverType, interfaceType)[GraphQLResolver::class.java.typeParameters[0]] 25 | 26 | if(type == null || type !is Class<*>) { 27 | throw ResolverError("Unable to determine data class for resolver '${resolverType.name}' from generic interface! This is most likely a bug with graphql-java-tools.") 28 | } 29 | 30 | if(type == Void::class.java) { 31 | throw ResolverError("Resolvers may not have ${Void::class.java.name} as their type, use a real type or use a root resolver interface.") 32 | } 33 | 34 | return type 35 | } 36 | 37 | override fun getFieldSearches(): List { 38 | return listOf( 39 | FieldResolverScanner.Search(resolverType, this, resolver, dataClassType, true), 40 | FieldResolverScanner.Search(dataClassType, this, null) 41 | ) 42 | } 43 | } 44 | 45 | internal class RootResolverInfo(val resolvers: List, private val options: SchemaParserOptions): ResolverInfo() { 46 | override fun getFieldSearches() = 47 | resolvers.map { FieldResolverScanner.Search(getRealResolverClass(it, options), this, it) } 48 | } 49 | 50 | internal class DataClassResolverInfo(private val dataClass: Class<*>): ResolverInfo() { 51 | override fun getFieldSearches() = 52 | listOf(FieldResolverScanner.Search(dataClass, this, null)) 53 | } 54 | 55 | internal class MissingResolverInfo: ResolverInfo() { 56 | override fun getFieldSearches(): List = listOf() 57 | } 58 | 59 | class ResolverError(message: String, cause: Throwable? = null) : RuntimeException(message, cause) 60 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/RootTypeInfo.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.language.SchemaDefinition 4 | import graphql.language.TypeName 5 | 6 | /** 7 | * @author Andrew Potter 8 | */ 9 | internal class RootTypeInfo private constructor(val queryType: TypeName?, val mutationType: TypeName?, val subscriptionType: TypeName?) { 10 | companion object { 11 | const val DEFAULT_QUERY_NAME = "Query" 12 | const val DEFAULT_MUTATION_NAME = "Mutation" 13 | const val DEFAULT_SUBSCRIPTION_NAME = "Subscription" 14 | 15 | fun fromSchemaDefinitions(definitions: List): RootTypeInfo { 16 | val queryType = definitions.lastOrNull()?.operationTypeDefinitions?.find { it.name == "query" }?.type as TypeName? 17 | val mutationType = definitions.lastOrNull()?.operationTypeDefinitions?.find { it.name == "mutation" }?.type as TypeName? 18 | val subscriptionType = definitions.lastOrNull()?.operationTypeDefinitions?.find { it.name == "subscription" }?.type as TypeName? 19 | 20 | return RootTypeInfo(queryType, mutationType, subscriptionType) 21 | } 22 | } 23 | 24 | fun getQueryName() = queryType?.name ?: DEFAULT_QUERY_NAME 25 | fun getMutationName() = mutationType?.name ?: DEFAULT_MUTATION_NAME 26 | fun getSubscriptionName() = subscriptionType?.name ?: DEFAULT_SUBSCRIPTION_NAME 27 | 28 | fun isMutationRequired() = mutationType != null 29 | fun isSubscriptionRequired() = subscriptionType != null 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/ScannedSchemaObjects.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import com.google.common.collect.BiMap 4 | import graphql.language.FieldDefinition 5 | import graphql.language.ObjectTypeDefinition 6 | import graphql.language.TypeDefinition 7 | import graphql.schema.GraphQLScalarType 8 | 9 | /** 10 | * @author Andrew Potter 11 | */ 12 | internal data class ScannedSchemaObjects( 13 | val dictionary: TypeClassDictionary, 14 | val definitions: Set, 15 | val customScalars: CustomScalarMap, 16 | val rootInfo: RootTypeInfo, 17 | val fieldResolversByType: Map> 18 | ) 19 | 20 | internal typealias TypeClassDictionary = BiMap> 21 | internal typealias CustomScalarMap = Map 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import com.google.common.collect.BiMap 4 | import com.google.common.collect.HashBiMap 5 | import com.google.common.collect.Maps 6 | import graphql.language.Definition 7 | import graphql.language.FieldDefinition import graphql.language.InputObjectTypeDefinition 8 | import graphql.language.InputValueDefinition 9 | import graphql.language.InterfaceTypeDefinition 10 | import graphql.language.ObjectTypeDefinition 11 | import graphql.language.ScalarTypeDefinition 12 | import graphql.language.SchemaDefinition 13 | import graphql.language.TypeDefinition 14 | import graphql.language.TypeExtensionDefinition 15 | import graphql.language.TypeName 16 | import graphql.language.UnionTypeDefinition 17 | import graphql.schema.GraphQLScalarType 18 | import graphql.schema.idl.ScalarInfo 19 | import org.slf4j.LoggerFactory 20 | import java.lang.reflect.Field 21 | import java.lang.reflect.Method 22 | 23 | /** 24 | * @author Andrew Potter 25 | */ 26 | internal class SchemaClassScanner(initialDictionary: BiMap>, allDefinitions: List, resolvers: List>, private val scalars: CustomScalarMap, private val options: SchemaParserOptions) { 27 | 28 | companion object { 29 | val log = LoggerFactory.getLogger(SchemaClassScanner::class.java)!! 30 | } 31 | 32 | private val rootInfo = RootTypeInfo.fromSchemaDefinitions(allDefinitions.filterIsInstance()) 33 | 34 | private val queryResolvers = resolvers.filterIsInstance() 35 | private val mutationResolvers = resolvers.filterIsInstance() 36 | private val subscriptionResolvers = resolvers.filterIsInstance() 37 | 38 | private val resolverInfos = resolvers.minus(queryResolvers).minus(mutationResolvers).minus(subscriptionResolvers).map { NormalResolverInfo(it, options) } 39 | private val resolverInfosByDataClass = this.resolverInfos.associateBy { it.dataClassType } 40 | 41 | private val initialDictionary = initialDictionary.mapValues { InitialDictionaryEntry(it.value) } 42 | private val extensionDefinitions = allDefinitions.filterIsInstance() 43 | 44 | private val definitionsByName = (allDefinitions.filterIsInstance() - extensionDefinitions).associateBy { it.name } 45 | private val objectDefinitions = (allDefinitions.filterIsInstance() - extensionDefinitions) 46 | private val objectDefinitionsByName = objectDefinitions.associateBy { it.name } 47 | private val interfaceDefinitionsByName = allDefinitions.filterIsInstance().associateBy { it.name } 48 | 49 | private val fieldResolverScanner = FieldResolverScanner(options) 50 | private val typeClassMatcher = TypeClassMatcher(definitionsByName) 51 | private val dictionary = mutableMapOf() 52 | private val unvalidatedTypes = mutableSetOf() 53 | private val queue = linkedSetOf() 54 | 55 | private val fieldResolversByType = mutableMapOf>() 56 | 57 | init { 58 | initialDictionary.forEach { (name, clazz) -> 59 | if(!definitionsByName.containsKey(name)) { 60 | throw SchemaClassScannerError("Class in supplied dictionary '${clazz.name}' specified type name '$name', but a type definition with that name was not found!") 61 | } 62 | } 63 | 64 | if(options.allowUnimplementedResolvers) { 65 | log.warn("Option 'allowUnimplementedResolvers' should only be set to true during development, as it can cause schema errors to be moved to query time instead of schema creation time. Make sure this is turned off in production.") 66 | } 67 | } 68 | 69 | /** 70 | * Attempts to discover GraphQL Type -> Java Class relationships by matching return types/argument types on known fields 71 | */ 72 | fun scanForClasses(): ScannedSchemaObjects { 73 | 74 | // Figure out what query, mutation and subscription types are called 75 | val rootTypeHolder = RootTypesHolder(options, rootInfo, definitionsByName, queryResolvers, mutationResolvers, subscriptionResolvers) 76 | 77 | handleRootType(rootTypeHolder.query) 78 | handleRootType(rootTypeHolder.mutation) 79 | handleRootType(rootTypeHolder.subscription) 80 | 81 | scanQueue() 82 | 83 | // Loop over all objects scanning each one only once for more objects to discover. 84 | do { 85 | do { 86 | // Require all implementors of discovered interfaces to be discovered or provided. 87 | handleInterfaceOrUnionSubTypes(getAllObjectTypesImplementingDiscoveredInterfaces(), { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." }) 88 | } while (scanQueue()) 89 | 90 | // Require all members of discovered unions to be discovered. 91 | handleInterfaceOrUnionSubTypes(getAllObjectTypeMembersOfDiscoveredUnions(), { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." }) 92 | } while (scanQueue()) 93 | 94 | return validateAndCreateResult(rootTypeHolder) 95 | } 96 | 97 | private fun scanQueue(): Boolean { 98 | if(queue.isEmpty()) { 99 | return false 100 | } 101 | 102 | while (queue.isNotEmpty()) { 103 | scanQueueItemForPotentialMatches(queue.iterator().run { val item = next(); remove(); item }) 104 | } 105 | 106 | return true 107 | } 108 | 109 | /** 110 | * Adds all root resolvers for a type to the list of classes to scan 111 | */ 112 | private fun handleRootType(rootType: RootType?) { 113 | if(rootType == null) { 114 | return 115 | } 116 | 117 | unvalidatedTypes.add(rootType.type) 118 | scanInterfacesOfType(rootType.type) 119 | scanResolverInfoForPotentialMatches(rootType.type, rootType.resolverInfo) 120 | } 121 | 122 | private fun validateAndCreateResult(rootTypeHolder: RootTypesHolder): ScannedSchemaObjects { 123 | initialDictionary.filter { !it.value.accessed }.forEach { 124 | log.warn("Dictionary mapping was provided but never used, and can be safely deleted: \"${it.key}\" -> ${it.value.get().name}") 125 | } 126 | 127 | val observedDefinitions = dictionary.keys.toSet() + unvalidatedTypes 128 | 129 | // The dictionary doesn't need to know what classes are used with scalars. 130 | // In addition, scalars can have duplicate classes so that breaks the bi-map. 131 | // Input types can also be excluded from the dictionary, since it's only used for interfaces, unions, and enums. 132 | // Union types can also be excluded, as their possible types are resolved recursively later 133 | val dictionary = try { 134 | Maps.unmodifiableBiMap(HashBiMap.create>().also { 135 | dictionary.filter { it.value.typeClass != null && it.key !is InputObjectTypeDefinition && it.key !is UnionTypeDefinition}.mapValuesTo(it) { it.value.typeClass } 136 | }) 137 | } catch (t: Throwable) { 138 | throw SchemaClassScannerError("Error creating bimap of type => class", t) 139 | } 140 | val scalarDefinitions = observedDefinitions.filterIsInstance() 141 | 142 | // Ensure all scalar definitions have implementations and add the definition to those. 143 | val scalars = scalarDefinitions.filter { !ScalarInfo.STANDARD_SCALAR_DEFINITIONS.containsKey(it.name) }.map { definition -> 144 | val provided = scalars[definition.name] ?: throw SchemaClassScannerError("Expected a user-defined GraphQL scalar type with name '${definition.name}' but found none!") 145 | GraphQLScalarType(provided.name, SchemaParser.getDocumentation(definition) ?: provided.description, provided.coercing, definition) 146 | }.associateBy { it.name!! } 147 | 148 | (definitionsByName.values - observedDefinitions).forEach { definition -> 149 | log.warn("Schema type was defined but can never be accessed, and can be safely deleted: ${definition.name}") 150 | } 151 | 152 | val fieldResolvers = fieldResolversByType.flatMap { it.value.map { it.value } } 153 | val observedNormalResolverInfos = fieldResolvers.map { it.resolverInfo }.distinct().filterIsInstance() 154 | 155 | (resolverInfos - observedNormalResolverInfos).forEach { resolverInfo -> 156 | log.warn("Resolver was provided but no methods on it were used in data fetchers, and can be safely deleted: ${resolverInfo.resolver}") 157 | } 158 | 159 | validateRootResolversWereUsed(rootTypeHolder.query, fieldResolvers) 160 | validateRootResolversWereUsed(rootTypeHolder.mutation, fieldResolvers) 161 | validateRootResolversWereUsed(rootTypeHolder.subscription, fieldResolvers) 162 | 163 | return ScannedSchemaObjects(dictionary, observedDefinitions + extensionDefinitions, scalars, rootInfo, fieldResolversByType.toMap()) 164 | } 165 | 166 | private fun validateRootResolversWereUsed(rootType: RootType?, fieldResolvers: List) { 167 | if(rootType == null) { 168 | return 169 | } 170 | 171 | val observedRootTypes = fieldResolvers.filter { it.resolverInfo is RootResolverInfo && it.resolverInfo == rootType.resolverInfo }.map { it.search.type }.toSet() 172 | rootType.resolvers.forEach { resolver -> 173 | if(rootType.resolverInfo.getRealResolverClass(resolver, options) !in observedRootTypes) { 174 | log.warn("Root ${rootType.name} resolver was provided but no methods on it were used in data fetchers for GraphQL type '${rootType.type.name}'! Either remove the ${rootType.resolverInterface.name} interface from the resolver or remove the resolver entirely: $resolver") 175 | } 176 | } 177 | } 178 | 179 | private fun getAllObjectTypesImplementingDiscoveredInterfaces(): List { 180 | return dictionary.keys.filterIsInstance().map { iface -> 181 | objectDefinitions.filter { obj -> obj.implements.filterIsInstance().any { it.name == iface.name } } 182 | }.flatten().distinctBy{ it.name } 183 | } 184 | 185 | private fun getAllObjectTypeMembersOfDiscoveredUnions(): List { 186 | val unionTypeNames = dictionary.keys.filterIsInstance().map { union -> union.name }.toSet() 187 | return dictionary.keys.filterIsInstance().map { union -> 188 | union.memberTypes.filterIsInstance().filter { !unionTypeNames.contains(it.name) }.map { objectDefinitionsByName[it.name] ?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: $union") } 189 | }.flatten().distinct() 190 | } 191 | 192 | private fun handleInterfaceOrUnionSubTypes(types: List, failureMessage: (ObjectTypeDefinition) -> String) { 193 | types.forEach { type -> 194 | val dictionaryContainsType = dictionary.filter{ it.key.name == type.name }.isNotEmpty() 195 | if(!unvalidatedTypes.contains(type) && !dictionaryContainsType) { 196 | val initialEntry = initialDictionary[type.name] ?: throw SchemaClassScannerError(failureMessage(type)) 197 | handleFoundType(type, initialEntry.get(), DictionaryReference()) 198 | } 199 | } 200 | } 201 | 202 | /** 203 | * Scan a new object for types that haven't been mapped yet. 204 | */ 205 | private fun scanQueueItemForPotentialMatches(item: QueueItem) { 206 | scanResolverInfoForPotentialMatches(item.type, resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)) 207 | } 208 | 209 | private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) { 210 | type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field -> 211 | val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo) 212 | 213 | fieldResolversByType.getOrPut(type, { mutableMapOf() })[fieldResolver.field] = fieldResolver 214 | fieldResolver.scanForMatches().forEach { potentialMatch -> 215 | handleFoundType(typeClassMatcher.match(potentialMatch)) 216 | } 217 | } 218 | } 219 | 220 | private fun handleFoundType(match: TypeClassMatcher.Match) { 221 | when(match) { 222 | is TypeClassMatcher.ScalarMatch -> { 223 | handleFoundScalarType(match.type) 224 | } 225 | 226 | is TypeClassMatcher.ValidMatch -> { 227 | handleFoundType(match.type, match.clazz, match.reference) 228 | } 229 | } 230 | } 231 | 232 | private fun handleFoundScalarType(type: ScalarTypeDefinition) { 233 | unvalidatedTypes.add(type) 234 | } 235 | 236 | /** 237 | * Enter a found type into the dictionary if it doesn't exist yet, add a reference pointing back to where it was discovered. 238 | */ 239 | private fun handleFoundType(type: TypeDefinition, clazz: Class<*>?, reference: Reference) { 240 | val realEntry = dictionary.getOrPut(type, { DictionaryEntry() }) 241 | var typeWasSet = false 242 | 243 | if (clazz != null) { 244 | typeWasSet = realEntry.setTypeIfMissing(clazz) 245 | 246 | if (realEntry.typeClass != clazz) { 247 | throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- $clazz:\n| ${reference.getDescription()}") 248 | } 249 | } 250 | 251 | realEntry.addReference(reference) 252 | 253 | // Check if we just added the entry... a little odd, but it works (and thread-safe, FWIW) 254 | if (typeWasSet && clazz != null) { 255 | handleNewType(type, clazz) 256 | } 257 | } 258 | 259 | /** 260 | * Handle a newly found type, adding it to the list of actually used types and putting it in the scanning queue if it's an object type. 261 | */ 262 | private fun handleNewType(graphQLType: TypeDefinition, javaType: Class<*>) { 263 | when(graphQLType) { 264 | is ObjectTypeDefinition -> { 265 | enqueue(graphQLType, javaType) 266 | scanInterfacesOfType(graphQLType) 267 | } 268 | 269 | is InputObjectTypeDefinition -> { 270 | graphQLType.inputValueDefinitions.forEach { inputValueDefinition -> 271 | findInputValueType(inputValueDefinition.name, javaType)?.let { inputValueJavaType -> 272 | val inputGraphQLType = inputValueDefinition.type.unwrap() 273 | if(inputGraphQLType is TypeName && !ScalarInfo.STANDARD_SCALAR_DEFINITIONS.containsKey(inputGraphQLType.name)) { 274 | handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType( 275 | inputValueDefinition.type, 276 | inputValueJavaType, 277 | GenericType(javaType, options).relativeToType(inputValueJavaType), 278 | InputObjectReference(inputValueDefinition), 279 | false 280 | ))) 281 | } 282 | } 283 | } 284 | } 285 | } 286 | } 287 | 288 | private fun scanInterfacesOfType(graphQLType: ObjectTypeDefinition) { 289 | graphQLType.implements.forEach { 290 | if(it is TypeName) { 291 | handleFoundType(interfaceDefinitionsByName[it.name] ?: throw SchemaClassScannerError("Object type ${graphQLType.name} declared interface ${it.name}, but no interface with that name was found in the schema!"), null, InterfaceReference(graphQLType)) 292 | } 293 | } 294 | } 295 | 296 | private fun enqueue(graphQLType: ObjectTypeDefinition, javaType: Class<*>) { 297 | queue.add(QueueItem(graphQLType, javaType)) 298 | } 299 | 300 | private fun findInputValueType(name: String, clazz: Class<*>): JavaType? { 301 | val methods = clazz.methods 302 | 303 | return (methods.find { 304 | it.name == name 305 | } ?: methods.find { 306 | it.name == "get${name.capitalize()}" 307 | })?.genericReturnType ?: clazz.fields.find { 308 | it.name == name 309 | }?.genericType 310 | } 311 | 312 | private data class QueueItem(val type: ObjectTypeDefinition, val clazz: Class<*>) 313 | 314 | private class DictionaryEntry { 315 | private val references = mutableListOf() 316 | var typeClass: Class<*>? = null 317 | private set 318 | 319 | fun setTypeIfMissing(typeClass: Class<*>): Boolean { 320 | if(this.typeClass == null) { 321 | this.typeClass = typeClass 322 | return true 323 | } 324 | 325 | return false 326 | } 327 | 328 | fun addReference(reference: Reference) { 329 | references.add(reference) 330 | } 331 | 332 | fun joinReferences() = "- $typeClass:\n| " + references.joinToString("\n| ") { it.getDescription() } 333 | } 334 | 335 | abstract class Reference { 336 | abstract fun getDescription(): String 337 | override fun toString() = getDescription() 338 | } 339 | 340 | private class DictionaryReference: Reference() { 341 | override fun getDescription() = "provided dictionary" 342 | } 343 | 344 | private class InterfaceReference(private val type: ObjectTypeDefinition): Reference() { 345 | override fun getDescription() = "interface declarations of ${type.name}" 346 | } 347 | 348 | private class InputObjectReference(private val type: InputValueDefinition): Reference() { 349 | override fun getDescription() = "input object $type" 350 | } 351 | 352 | private class InitialDictionaryEntry(private val clazz: Class<*>) { 353 | var accessed = false 354 | private set 355 | 356 | fun get(): Class<*> { 357 | accessed = true 358 | return clazz 359 | } 360 | } 361 | 362 | class ReturnValueReference(private val method: Method): Reference() { 363 | override fun getDescription() = "return type of method $method" 364 | } 365 | 366 | class MethodParameterReference(private val method: Method, private val index: Int): Reference() { 367 | override fun getDescription() = "parameter $index of method $method" 368 | } 369 | 370 | class FieldTypeReference(private val field: Field): Reference() { 371 | override fun getDescription() = "type of field $field" 372 | } 373 | 374 | class RootTypesHolder(options: SchemaParserOptions, rootInfo: RootTypeInfo, definitionsByName: Map, queryResolvers: List, mutationResolvers: List, subscriptionResolvers: List) { 375 | private val queryName = rootInfo.getQueryName() 376 | private val mutationName = rootInfo.getMutationName() 377 | private val subscriptionName = rootInfo.getSubscriptionName() 378 | 379 | private val queryDefinition = definitionsByName[queryName] 380 | private val mutationDefinition = definitionsByName[mutationName] 381 | private val subscriptionDefinition = definitionsByName[subscriptionName] 382 | 383 | private val queryResolverInfo = RootResolverInfo(queryResolvers, options) 384 | private val mutationResolverInfo = RootResolverInfo(mutationResolvers, options) 385 | private val subscriptionResolverInfo = RootResolverInfo(subscriptionResolvers, options) 386 | 387 | val query = createRootType("query", queryDefinition, queryName, true, queryResolvers, GraphQLQueryResolver::class.java, queryResolverInfo) 388 | val mutation = createRootType("mutation", mutationDefinition, mutationName, rootInfo.isMutationRequired(), mutationResolvers, GraphQLMutationResolver::class.java, mutationResolverInfo) 389 | val subscription = createRootType("subscription", subscriptionDefinition, subscriptionName, rootInfo.isSubscriptionRequired(), subscriptionResolvers, GraphQLSubscriptionResolver::class.java, subscriptionResolverInfo) 390 | 391 | private fun createRootType(name: String, type: TypeDefinition?, typeName: String, required: Boolean, resolvers: List, resolverInterface: Class<*>, resolverInfo: RootResolverInfo): RootType? { 392 | if(type == null) { 393 | if(required) { 394 | throw SchemaClassScannerError("Type definition for root $name type '$typeName' not found!") 395 | } 396 | 397 | return null 398 | } 399 | 400 | if(type !is ObjectTypeDefinition) { 401 | throw SchemaClassScannerError("Expected root query type's type to be ${ObjectTypeDefinition::class.java.simpleName}, but it was ${type.javaClass.simpleName}") 402 | } 403 | 404 | // Find query resolver class 405 | if(resolvers.isEmpty()) { 406 | throw SchemaClassScannerError("No Root resolvers for $name type '$typeName' found! Provide one or more ${resolverInterface.name} to the builder.") 407 | } 408 | 409 | return RootType(name, type, resolvers, resolverInterface, resolverInfo) 410 | } 411 | } 412 | 413 | class RootType(val name: String, val type: ObjectTypeDefinition, val resolvers: List, val resolverInterface: Class<*>, val resolverInfo: RootResolverInfo) 414 | } 415 | 416 | class SchemaClassScannerError(message: String, throwable: Throwable? = null) : RuntimeException(message, throwable) 417 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/SchemaObjects.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.schema.GraphQLObjectType 4 | import graphql.schema.GraphQLSchema 5 | import graphql.schema.GraphQLType 6 | 7 | /** 8 | * @author Andrew Potter 9 | */ 10 | data class SchemaObjects(val query: GraphQLObjectType, val mutation: GraphQLObjectType?, val subscription: GraphQLObjectType?, val dictionary: Set) { 11 | 12 | /** 13 | * Makes a GraphQLSchema with query, mutation and subscription. 14 | */ 15 | fun toSchema(): GraphQLSchema = GraphQLSchema.newSchema() 16 | .query(query) 17 | .mutation(mutation) 18 | .subscription(subscription) 19 | .build(dictionary) 20 | 21 | /** 22 | * Makes a GraphQLSchema with query but without mutation and subscription. 23 | */ 24 | fun toReadOnlySchema(): GraphQLSchema = GraphQLSchema.newSchema() 25 | .query(query) 26 | .build(dictionary) 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.language.AbstractNode 4 | import graphql.language.ArrayValue 5 | import graphql.language.BooleanValue 6 | import graphql.language.Directive 7 | import graphql.language.EnumTypeDefinition 8 | import graphql.language.EnumValue 9 | import graphql.language.FieldDefinition 10 | import graphql.language.FloatValue 11 | import graphql.language.InputObjectTypeDefinition 12 | import graphql.language.IntValue 13 | import graphql.language.InterfaceTypeDefinition 14 | import graphql.language.ListType 15 | import graphql.language.NonNullType 16 | import graphql.language.ObjectTypeDefinition 17 | import graphql.language.ObjectValue 18 | import graphql.language.StringValue 19 | import graphql.language.Type 20 | import graphql.language.TypeExtensionDefinition 21 | import graphql.language.TypeName 22 | import graphql.language.UnionTypeDefinition 23 | import graphql.language.Value 24 | import graphql.schema.GraphQLEnumType 25 | import graphql.schema.GraphQLFieldDefinition 26 | import graphql.schema.GraphQLInputObjectType 27 | import graphql.schema.GraphQLInputType 28 | import graphql.schema.GraphQLInterfaceType 29 | import graphql.schema.GraphQLList 30 | import graphql.schema.GraphQLNonNull 31 | import graphql.schema.GraphQLObjectType 32 | import graphql.schema.GraphQLOutputType 33 | import graphql.schema.GraphQLSchema 34 | import graphql.schema.GraphQLType 35 | import graphql.schema.GraphQLTypeReference 36 | import graphql.schema.GraphQLUnionType 37 | import graphql.schema.TypeResolverProxy 38 | import graphql.schema.idl.ScalarInfo 39 | import kotlin.reflect.KClass 40 | 41 | /** 42 | * Parses a GraphQL Schema and maps object fields to provided class methods. 43 | * 44 | * @author Andrew Potter 45 | */ 46 | class SchemaParser internal constructor(scanResult: ScannedSchemaObjects) { 47 | 48 | companion object { 49 | val DEFAULT_DEPRECATION_MESSAGE = "No longer supported" 50 | 51 | @JvmStatic fun newParser() = SchemaParserBuilder() 52 | internal fun getDocumentation(node: AbstractNode): String? = node.comments?.map { it.content.trim() }?.joinToString("\n") 53 | } 54 | 55 | private val dictionary = scanResult.dictionary 56 | private val definitions = scanResult.definitions 57 | private val customScalars = scanResult.customScalars 58 | private val rootInfo = scanResult.rootInfo 59 | private val fieldResolversByType = scanResult.fieldResolversByType 60 | 61 | private val extensionDefinitions = definitions.filterIsInstance() 62 | private val objectDefinitions = (definitions.filterIsInstance() - extensionDefinitions) 63 | 64 | private val inputObjectDefinitions = definitions.filterIsInstance() 65 | private val enumDefinitions = definitions.filterIsInstance() 66 | private val interfaceDefinitions = definitions.filterIsInstance() 67 | private val unionDefinitions = definitions.filterIsInstance() 68 | 69 | private val permittedTypesForObject: Set = (objectDefinitions.map { it.name } + 70 | enumDefinitions.map { it.name } + 71 | interfaceDefinitions.map { it.name } + 72 | unionDefinitions.map { it.name }).toSet() 73 | private val permittedTypesForInputObject: Set = 74 | (inputObjectDefinitions.map { it.name } + enumDefinitions.map { it.name }).toSet() 75 | 76 | /** 77 | * Parses the given schema with respect to the given dictionary and returns GraphQL objects. 78 | */ 79 | fun parseSchemaObjects(): SchemaObjects { 80 | 81 | // Create GraphQL objects 82 | val interfaces = interfaceDefinitions.map { createInterfaceObject(it) } 83 | val objects = objectDefinitions.map { createObject(it, interfaces) } 84 | val unions = unionDefinitions.map { createUnionObject(it, objects) } 85 | val inputObjects = inputObjectDefinitions.map { createInputObject(it) } 86 | val enums = enumDefinitions.map { createEnumObject(it) } 87 | 88 | // Assign type resolver to interfaces now that we know all of the object types 89 | interfaces.forEach { (it.typeResolver as TypeResolverProxy).typeResolver = InterfaceTypeResolver(dictionary.inverse(), it, objects) } 90 | unions.forEach { (it.typeResolver as TypeResolverProxy).typeResolver = UnionTypeResolver(dictionary.inverse(), it, objects) } 91 | 92 | // Find query type and mutation/subscription type (if mutation/subscription type exists) 93 | val queryName = rootInfo.getQueryName() 94 | val mutationName = rootInfo.getMutationName() 95 | val subscriptionName = rootInfo.getSubscriptionName() 96 | 97 | val query = objects.find { it.name == queryName } ?: throw SchemaError("Expected a Query object with name '$queryName' but found none!") 98 | val mutation = objects.find { it.name == mutationName } ?: if(rootInfo.isMutationRequired()) throw SchemaError("Expected a Mutation object with name '$mutationName' but found none!") else null 99 | val subscription = objects.find { it.name == subscriptionName } ?: if (rootInfo.isSubscriptionRequired()) throw SchemaError("Expected a Subscription object with name '$subscriptionName' but found none!") else null 100 | 101 | return SchemaObjects(query, mutation, subscription, (objects + inputObjects + enums + interfaces + unions).toSet()) 102 | } 103 | 104 | /** 105 | * Parses the given schema with respect to the given dictionary and returns a GraphQLSchema 106 | */ 107 | fun makeExecutableSchema(): GraphQLSchema = parseSchemaObjects().toSchema() 108 | 109 | private fun createObject(definition: ObjectTypeDefinition, interfaces: List): GraphQLObjectType { 110 | val name = definition.name 111 | val builder = GraphQLObjectType.newObject() 112 | .name(name) 113 | .definition(definition) 114 | .description(getDocumentation(definition)) 115 | 116 | definition.implements.forEach { implementsDefinition -> 117 | val interfaceName = (implementsDefinition as TypeName).name 118 | builder.withInterface(interfaces.find { it.name == interfaceName } ?: throw SchemaError("Expected interface type with name '$interfaceName' but found none!")) 119 | } 120 | 121 | definition.getExtendedFieldDefinitions(extensionDefinitions).forEach { fieldDefinition -> 122 | builder.field { field -> 123 | createField(field, fieldDefinition) 124 | 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")) 125 | } 126 | } 127 | 128 | return builder.build() 129 | } 130 | 131 | private fun createInputObject(definition: InputObjectTypeDefinition): GraphQLInputObjectType { 132 | val builder = GraphQLInputObjectType.newInputObject() 133 | .name(definition.name) 134 | .definition(definition) 135 | .description(getDocumentation(definition)) 136 | 137 | definition.inputValueDefinitions.forEach { inputDefinition -> 138 | builder.field { field -> 139 | field.name(inputDefinition.name) 140 | field.definition(inputDefinition) 141 | field.description(getDocumentation(inputDefinition)) 142 | field.defaultValue(inputDefinition.defaultValue) 143 | field.type(determineInputType(inputDefinition.type)) 144 | } 145 | } 146 | 147 | return builder.build() 148 | } 149 | 150 | private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType { 151 | val name = definition.name 152 | val type = dictionary[definition] ?: throw SchemaError("Expected enum with name '$name' but found none!") 153 | if (!type.isEnum) throw SchemaError("Type '$name' is declared as an enum in the GraphQL schema but is not a Java enum!") 154 | 155 | val builder = GraphQLEnumType.newEnum() 156 | .name(name) 157 | .definition(definition) 158 | .description(getDocumentation(definition)) 159 | 160 | definition.enumValueDefinitions.forEach { enumDefinition -> 161 | val enumName = enumDefinition.name 162 | val enumValue = type.enumConstants.find { (it as Enum<*>).name == enumName } ?: throw SchemaError("Expected value for name '$enumName' in enum '${type.simpleName}' but found none!") 163 | getDeprecated(enumDefinition.directives).let { 164 | when (it) { 165 | is String -> builder.value(enumName, enumValue, getDocumentation(enumDefinition), it) 166 | else -> builder.value(enumName, enumValue, getDocumentation(enumDefinition)) 167 | } 168 | } 169 | } 170 | 171 | return builder.build() 172 | } 173 | 174 | private fun createInterfaceObject(definition: InterfaceTypeDefinition): GraphQLInterfaceType { 175 | val name = definition.name 176 | val builder = GraphQLInterfaceType.newInterface() 177 | .name(name) 178 | .definition(definition) 179 | .description(getDocumentation(definition)) 180 | .typeResolver(TypeResolverProxy()) 181 | 182 | definition.fieldDefinitions.forEach { fieldDefinition -> 183 | builder.field { field -> createField(field, fieldDefinition) } 184 | } 185 | 186 | return builder.build() 187 | } 188 | 189 | private fun createUnionObject(definition: UnionTypeDefinition, types: List): GraphQLUnionType { 190 | val name = definition.name 191 | val builder = GraphQLUnionType.newUnionType() 192 | .name(name) 193 | .definition(definition) 194 | .description(getDocumentation(definition)) 195 | .typeResolver(TypeResolverProxy()) 196 | 197 | getLeafUnionObjects(definition, types).forEach { builder.possibleType(it) } 198 | return builder.build() 199 | } 200 | 201 | private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List): List { 202 | val name = definition.name 203 | val leafObjects = mutableListOf() 204 | 205 | definition.memberTypes.forEach { 206 | val typeName = (it as TypeName).name 207 | 208 | // Is this a nested union? If so, expand 209 | val nestedUnion : UnionTypeDefinition? = unionDefinitions.find { otherDefinition -> typeName == otherDefinition.name } 210 | 211 | if (nestedUnion != null) { 212 | leafObjects.addAll(getLeafUnionObjects(nestedUnion, types)) 213 | } else { 214 | leafObjects.add(types.find { it.name == typeName } ?: throw SchemaError("Expected object type '$typeName' for union type '$name', but found none!")) 215 | } 216 | } 217 | return leafObjects 218 | } 219 | 220 | private fun createField(field: GraphQLFieldDefinition.Builder, fieldDefinition : FieldDefinition): GraphQLFieldDefinition.Builder { 221 | field.name(fieldDefinition.name) 222 | field.description(getDocumentation(fieldDefinition)) 223 | field.definition(fieldDefinition) 224 | getDeprecated(fieldDefinition.directives)?.let { field.deprecate(it) } 225 | field.type(determineOutputType(fieldDefinition.type)) 226 | fieldDefinition.inputValueDefinitions.forEach { argumentDefinition -> 227 | field.argument { argument -> 228 | argument.name(argumentDefinition.name) 229 | argument.definition(argumentDefinition) 230 | argument.description(getDocumentation(argumentDefinition)) 231 | argument.defaultValue(buildDefaultValue(argumentDefinition.defaultValue)) 232 | argument.type(determineInputType(argumentDefinition.type)) 233 | } 234 | } 235 | return field 236 | } 237 | 238 | private fun buildDefaultValue(value: Value?): Any? { 239 | return when(value) { 240 | null -> null 241 | is IntValue -> value.value 242 | is FloatValue -> value.value 243 | is StringValue -> value.value 244 | is EnumValue -> value.name 245 | is BooleanValue -> value.isValue 246 | is ArrayValue -> value.values.map { buildDefaultValue(it) }.toTypedArray() 247 | is ObjectValue -> value.objectFields.associate { it.name to buildDefaultValue(it.value) } 248 | else -> throw SchemaError("Unrecognized default value: $value") 249 | } 250 | } 251 | 252 | private fun determineOutputType(typeDefinition: Type) = 253 | determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject) as GraphQLOutputType 254 | private fun determineInputType(typeDefinition: Type) = 255 | determineType(GraphQLInputType::class, typeDefinition, permittedTypesForInputObject) as GraphQLInputType 256 | 257 | private fun determineType(expectedType: KClass, typeDefinition: Type, allowedTypeReferences: Set): GraphQLType = 258 | when (typeDefinition) { 259 | is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences)) 260 | is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences)) 261 | is TypeName -> { 262 | val scalarType = graphQLScalars[typeDefinition.name] ?: customScalars[typeDefinition.name] 263 | if (scalarType != null) { 264 | scalarType 265 | } else { 266 | if (!allowedTypeReferences.contains(typeDefinition.name)) { 267 | throw SchemaError("Expected type '${typeDefinition.name}' to be a ${expectedType.simpleName}, but it wasn't! " + 268 | "Was a type only permitted for object types incorrectly used as an input type, or vice-versa?") 269 | } 270 | GraphQLTypeReference(typeDefinition.name) 271 | } 272 | } 273 | else -> throw SchemaError("Unknown type: $typeDefinition") 274 | } 275 | 276 | /** 277 | * Returns an optional [String] describing a deprecated field/enum. 278 | * If a deprecation directive was defined using the @deprecated directive, 279 | * then a String containing either the contents of the 'reason' argument, if present, or a default 280 | * message defined in [DEFAULT_DEPRECATION_MESSAGE] will be returned. Otherwise, [null] will be returned 281 | * indicating no deprecation directive was found within the directives list. 282 | */ 283 | private fun getDeprecated(directives: List): String? = 284 | getDirective(directives, "deprecated")?.let { directive -> 285 | (directive.arguments.find { it.name == "reason" }?.value as? StringValue)?.value ?: 286 | DEFAULT_DEPRECATION_MESSAGE 287 | } 288 | 289 | private fun getDirective(directives: List, name: String): Directive? = directives.find { 290 | it.name == name 291 | } 292 | } 293 | 294 | class SchemaError(message: String, cause: Throwable? = null) : RuntimeException(message, cause) 295 | 296 | val graphQLScalars = ScalarInfo.STANDARD_SCALARS.associateBy { it.name } 297 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.google.common.collect.BiMap 5 | import com.google.common.collect.HashBiMap 6 | import com.google.common.collect.Maps 7 | import graphql.parser.Parser 8 | import graphql.schema.GraphQLScalarType 9 | import org.antlr.v4.runtime.RecognitionException 10 | import org.antlr.v4.runtime.misc.ParseCancellationException 11 | import org.reactivestreams.Publisher 12 | import java.util.concurrent.CompletableFuture 13 | import java.util.concurrent.CompletionStage 14 | import java.util.concurrent.Future 15 | import kotlin.reflect.KClass 16 | 17 | /** 18 | * @author Andrew Potter 19 | */ 20 | class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictionary = SchemaParserDictionary()) { 21 | 22 | private val schemaString = StringBuilder() 23 | private val resolvers = mutableListOf>() 24 | private val scalars = mutableListOf() 25 | private var options = SchemaParserOptions.defaultOptions() 26 | 27 | /** 28 | * Add GraphQL schema files from the classpath. 29 | */ 30 | fun files(vararg files: String) = this.apply { 31 | files.forEach { this.file(it) } 32 | } 33 | 34 | /** 35 | * Add a GraphQL Schema file from the classpath. 36 | */ 37 | fun file(filename: String) = this.apply { 38 | this.schemaString(java.io.BufferedReader(java.io.InputStreamReader( 39 | object : Any() {}.javaClass.classLoader.getResourceAsStream(filename) ?: throw java.io.FileNotFoundException("classpath:$filename") 40 | )).readText()) 41 | } 42 | 43 | /** 44 | * Add a GraphQL schema string directly. 45 | */ 46 | fun schemaString(string: String) = this.apply { 47 | schemaString.append("\n").append(string) 48 | } 49 | 50 | /** 51 | * Add GraphQLResolvers to the parser's dictionary. 52 | */ 53 | fun resolvers(vararg resolvers: GraphQLResolver<*>) = this.apply { 54 | this.resolvers.addAll(resolvers) 55 | } 56 | 57 | /** 58 | * Add GraphQLResolvers to the parser's dictionary. 59 | */ 60 | fun resolvers(resolvers: List>) = this.apply { 61 | this.resolvers.addAll(resolvers) 62 | } 63 | 64 | /** 65 | * Add scalars to the parser's dictionary. 66 | */ 67 | fun scalars(vararg scalars: GraphQLScalarType) = this.apply { 68 | this.scalars.addAll(scalars) 69 | } 70 | 71 | /** 72 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name. 73 | */ 74 | fun dictionary(name: String, clazz: Class<*>) = this.apply { 75 | this.dictionary.add(name, clazz) 76 | } 77 | 78 | /** 79 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name. 80 | */ 81 | fun dictionary(name: String, clazz: KClass<*>) = this.apply { 82 | this.dictionary.add(name, clazz) 83 | } 84 | 85 | /** 86 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name. 87 | */ 88 | fun dictionary(dictionary: Map>) = this.apply { 89 | this.dictionary.add(dictionary) 90 | } 91 | 92 | /** 93 | * Add arbitrary classes to the parser's dictionary. 94 | */ 95 | fun dictionary(clazz: Class<*>) = this.apply { 96 | this.dictionary.add(clazz) 97 | } 98 | 99 | /** 100 | * Add arbitrary classes to the parser's dictionary. 101 | */ 102 | fun dictionary(clazz: KClass<*>) = this.apply { 103 | this.dictionary.add(clazz) 104 | } 105 | 106 | /** 107 | * Add arbitrary classes to the parser's dictionary. 108 | */ 109 | fun dictionary(vararg dictionary: Class<*>) = this.apply { 110 | this.dictionary.add(*dictionary) 111 | } 112 | 113 | /** 114 | * Add arbitrary classes to the parser's dictionary. 115 | */ 116 | fun dictionary(vararg dictionary: KClass<*>) = this.apply { 117 | this.dictionary.add(*dictionary) 118 | } 119 | 120 | /** 121 | * Add arbitrary classes to the parser's dictionary. 122 | */ 123 | fun dictionary(dictionary: Collection>) = this.apply { 124 | this.dictionary.add(dictionary) 125 | } 126 | 127 | fun options(options: SchemaParserOptions) = this.apply { 128 | this.options = options 129 | } 130 | 131 | /** 132 | * Scan for classes with the supplied schema and dictionary. Used for testing. 133 | */ 134 | private fun scan(): ScannedSchemaObjects { 135 | val document = try { 136 | Parser().parseDocument(this.schemaString.toString()) 137 | } catch (pce: ParseCancellationException) { 138 | val cause = pce.cause 139 | if(cause != null && cause is RecognitionException) { 140 | throw InvalidSchemaError(pce, cause) 141 | } else { 142 | throw pce 143 | } 144 | } 145 | 146 | val definitions = document.definitions 147 | val customScalars = scalars.associateBy { it.name } 148 | 149 | return SchemaClassScanner(dictionary.getDictionary(), definitions, resolvers, customScalars, options).scanForClasses() 150 | } 151 | 152 | /** 153 | * Build the parser with the supplied schema and dictionary. 154 | */ 155 | fun build() = SchemaParser(scan()) 156 | } 157 | 158 | class InvalidSchemaError(pce: ParseCancellationException, private val recognitionException: RecognitionException): RuntimeException(pce) { 159 | override val message: String? 160 | get() = "Invalid schema provided (${recognitionException.javaClass.name}) at: ${recognitionException.offendingToken}" 161 | } 162 | 163 | class SchemaParserDictionary { 164 | 165 | private val dictionary: BiMap> = HashBiMap.create() 166 | 167 | fun getDictionary(): BiMap> = Maps.unmodifiableBiMap(dictionary) 168 | 169 | /** 170 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name. 171 | */ 172 | fun add(name: String, clazz: Class<*>) = this.apply { 173 | this.dictionary.put(name, clazz) 174 | } 175 | 176 | /** 177 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name. 178 | */ 179 | fun add(name: String, clazz: KClass<*>) = this.apply { 180 | this.dictionary.put(name, clazz.java) 181 | } 182 | 183 | /** 184 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name. 185 | */ 186 | fun add(dictionary: Map>) = this.apply { 187 | this.dictionary.putAll(dictionary) 188 | } 189 | 190 | /** 191 | * Add arbitrary classes to the parser's dictionary. 192 | */ 193 | fun add(clazz: Class<*>) = this.apply { 194 | this.add(clazz.simpleName, clazz) 195 | } 196 | 197 | /** 198 | * Add arbitrary classes to the parser's dictionary. 199 | */ 200 | fun add(clazz: KClass<*>) = this.apply { 201 | this.add(clazz.java.simpleName, clazz) 202 | } 203 | 204 | /** 205 | * Add arbitrary classes to the parser's dictionary. 206 | */ 207 | fun add(vararg dictionary: Class<*>) = this.apply { 208 | dictionary.forEach { this.add(it) } 209 | } 210 | 211 | /** 212 | * Add arbitrary classes to the parser's dictionary. 213 | */ 214 | fun add(vararg dictionary: KClass<*>) = this.apply { 215 | dictionary.forEach { this.add(it) } 216 | } 217 | 218 | /** 219 | * Add arbitrary classes to the parser's dictionary. 220 | */ 221 | fun add(dictionary: Collection>) = this.apply { 222 | dictionary.forEach { this.add(it) } 223 | } 224 | } 225 | 226 | data class SchemaParserOptions internal constructor(val genericWrappers: List, val allowUnimplementedResolvers: Boolean, val objectMapperConfigurer: ObjectMapperConfigurer, val proxyHandlers: List) { 227 | companion object { 228 | @JvmStatic fun newOptions() = Builder() 229 | @JvmStatic fun defaultOptions() = Builder().build() 230 | } 231 | 232 | class Builder { 233 | private val genericWrappers: MutableList = mutableListOf() 234 | private var useDefaultGenericWrappers = true 235 | private var allowUnimplementedResolvers = false 236 | private var objectMapperConfigurer: ObjectMapperConfigurer = ObjectMapperConfigurer { _, _ -> } 237 | private val proxyHandlers: MutableList = mutableListOf(Spring4AopProxyHandler(), GuiceAopProxyHandler()) 238 | 239 | fun genericWrappers(genericWrappers: List) = this.apply { 240 | this.genericWrappers.addAll(genericWrappers) 241 | } 242 | 243 | fun genericWrappers(vararg genericWrappers: GenericWrapper) = this.apply { 244 | this.genericWrappers.addAll(genericWrappers) 245 | } 246 | 247 | fun useDefaultGenericWrappers(useDefaultGenericWrappers: Boolean) = this.apply { 248 | this.useDefaultGenericWrappers = useDefaultGenericWrappers 249 | } 250 | 251 | fun allowUnimplementedResolvers(allowUnimplementedResolvers: Boolean) = this.apply { 252 | this.allowUnimplementedResolvers = allowUnimplementedResolvers 253 | } 254 | 255 | fun objectMapperConfigurer(objectMapperConfigurer: ObjectMapperConfigurer) = this.apply { 256 | this.objectMapperConfigurer = objectMapperConfigurer 257 | } 258 | 259 | fun objectMapperConfigurer(objectMapperConfigurer: (ObjectMapper, ObjectMapperConfigurerContext) -> Unit) = this.apply { 260 | this.objectMapperConfigurer(ObjectMapperConfigurer(objectMapperConfigurer)) 261 | } 262 | 263 | fun addProxyHandler(proxyHandler: ProxyHandler) = this.apply { 264 | this.proxyHandlers.add(proxyHandler) 265 | } 266 | 267 | fun build(): SchemaParserOptions { 268 | val wrappers = if(useDefaultGenericWrappers) { 269 | genericWrappers + listOf( 270 | GenericWrapper(Future::class, 0), 271 | GenericWrapper(CompletableFuture::class, 0), 272 | GenericWrapper(CompletionStage::class, 0), 273 | GenericWrapper(Publisher::class, 0) 274 | ) 275 | } else { 276 | genericWrappers 277 | } 278 | 279 | return SchemaParserOptions(wrappers, allowUnimplementedResolvers, objectMapperConfigurer, proxyHandlers) 280 | } 281 | } 282 | 283 | data class GenericWrapper(val type: Class<*>, val index: Int) { 284 | constructor(type: KClass<*>, index: Int): this(type.java, index) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/Spring4AopProxyHandler.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import org.springframework.aop.support.AopUtils 4 | 5 | /** 6 | * @author Andrew Potter 7 | */ 8 | 9 | class Spring4AopProxyHandler: ProxyHandler { 10 | 11 | val isEnabled: Boolean = 12 | try { 13 | Class.forName("org.springframework.aop.support.AopUtils") 14 | true 15 | } catch (_: ClassNotFoundException) { 16 | false 17 | } 18 | 19 | override fun canHandle(resolver: GraphQLResolver<*>?): Boolean { 20 | return isEnabled && AopUtils.isAopProxy(resolver) 21 | } 22 | 23 | override fun getTargetClass(resolver: GraphQLResolver<*>?): Class<*> = AopUtils.getTargetClass(resolver) 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/TypeClassMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.language.ListType 4 | import graphql.language.NonNullType 5 | import graphql.language.ScalarTypeDefinition 6 | import graphql.language.TypeDefinition 7 | import graphql.language.TypeName 8 | import graphql.schema.idl.ScalarInfo 9 | import org.apache.commons.lang3.reflect.TypeUtils 10 | import java.lang.reflect.ParameterizedType 11 | import java.util.Optional 12 | 13 | /** 14 | * @author Andrew Potter 15 | */ 16 | internal class TypeClassMatcher(private val definitionsByName: Map) { 17 | 18 | companion object { 19 | fun isListType(realType: ParameterizedType, generic: GenericType) = generic.isTypeAssignableFromRawClass(realType, Iterable::class.java) 20 | } 21 | 22 | private fun error(potentialMatch: PotentialMatch, msg: String) = SchemaClassScannerError("Unable to match type definition (${potentialMatch.graphQLType}) with java type (${potentialMatch.javaType}): $msg") 23 | 24 | fun match(potentialMatch: PotentialMatch): Match { 25 | return if(potentialMatch.batched) { 26 | match(stripBatchedType(potentialMatch)) // stripBatchedType sets 'batched' to false 27 | } else { 28 | match(potentialMatch, potentialMatch.graphQLType, potentialMatch.javaType, true) 29 | } 30 | } 31 | 32 | private fun match(potentialMatch: PotentialMatch, graphQLType: GraphQLLangType, javaType: JavaType, root: Boolean = false): Match { 33 | 34 | var realType = potentialMatch.generic.unwrapGenericType(javaType) 35 | var optional = false 36 | 37 | // Handle jdk8 Optionals 38 | if(realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) { 39 | optional = true 40 | 41 | if(potentialMatch.location == Location.RETURN_TYPE && !root) { 42 | throw error(potentialMatch, "${Optional::class.java.name} can only be used at the top level of a return type") 43 | } 44 | 45 | realType = potentialMatch.generic.unwrapGenericType(realType.actualTypeArguments.first()) 46 | 47 | if(realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) { 48 | throw error(potentialMatch, "${Optional::class.java.name} cannot be nested within itself") 49 | } 50 | } 51 | 52 | // Match graphql type to java type. 53 | return when(graphQLType) { 54 | is NonNullType -> { 55 | if(optional) { 56 | throw error(potentialMatch, "graphql type is marked as nonnull but ${Optional::class.java.name} was used") 57 | } 58 | match(potentialMatch, graphQLType.type, realType) 59 | } 60 | 61 | is ListType -> { 62 | if(realType is ParameterizedType && isListType(realType, potentialMatch)) { 63 | match(potentialMatch, graphQLType.type, realType.actualTypeArguments.first()) 64 | } else { 65 | throw error(potentialMatch, "Java class is not a List or generic type information was lost: $realType") 66 | } 67 | } 68 | 69 | is TypeName -> { 70 | val typeDefinition = ScalarInfo.STANDARD_SCALAR_DEFINITIONS[graphQLType.name] ?: definitionsByName[graphQLType.name] ?: throw error(potentialMatch, "No ${TypeDefinition::class.java.simpleName} for type name ${graphQLType.name}") 71 | if(typeDefinition is ScalarTypeDefinition) { 72 | ScalarMatch(typeDefinition) 73 | } else { 74 | ValidMatch(typeDefinition, requireRawClass(realType), potentialMatch.reference) 75 | } 76 | } 77 | 78 | is TypeDefinition -> ValidMatch(graphQLType, requireRawClass(realType), potentialMatch.reference) 79 | else -> throw error(potentialMatch, "Unknown type: ${realType.javaClass.name}") 80 | } 81 | } 82 | 83 | private fun isListType(realType: ParameterizedType, potentialMatch: PotentialMatch) = isListType(realType, potentialMatch.generic) 84 | 85 | private fun requireRawClass(type: JavaType): Class<*> { 86 | if(type !is Class<*>) { 87 | throw RawClassRequiredForGraphQLMappingException("Type ${TypeUtils.toString(type)} cannot be mapped to a GraphQL type! Since GraphQL-Java deals with erased types at runtime, only non-parameterized classes can represent a GraphQL type. This allows for reverse-lookup by java class in interfaces and union types.") 88 | } 89 | 90 | return type 91 | } 92 | 93 | private fun stripBatchedType(potentialMatch: PotentialMatch): PotentialMatch { 94 | val realType = potentialMatch.generic.unwrapGenericType(potentialMatch.javaType) 95 | 96 | if(realType is ParameterizedType && isListType(realType, potentialMatch)) { 97 | return potentialMatch.copy(javaType = realType.actualTypeArguments.first(), batched = false) 98 | } else { 99 | throw error(potentialMatch, "Method was marked as @Batched but ${potentialMatch.location.prettyName} was not a list!") 100 | } 101 | } 102 | 103 | internal interface Match 104 | internal data class ScalarMatch(val type: ScalarTypeDefinition): Match 105 | internal data class ValidMatch(val type: TypeDefinition, val clazz: Class<*>, val reference: SchemaClassScanner.Reference): Match 106 | internal enum class Location(val prettyName: String) { 107 | RETURN_TYPE("return type"), 108 | PARAMETER_TYPE("parameter"), 109 | } 110 | 111 | internal data class PotentialMatch(val graphQLType: GraphQLLangType, val javaType: JavaType, val generic: GenericType.RelativeTo, val reference: SchemaClassScanner.Reference, val location: Location, val batched: Boolean) { 112 | companion object { 113 | fun returnValue(graphQLType: GraphQLLangType, javaType: JavaType, generic: GenericType.RelativeTo, reference: SchemaClassScanner.Reference, batched: Boolean) = 114 | PotentialMatch(graphQLType, javaType, generic, reference, Location.RETURN_TYPE, batched) 115 | 116 | fun parameterType(graphQLType: GraphQLLangType, javaType: JavaType, generic: GenericType.RelativeTo, reference: SchemaClassScanner.Reference, batched: Boolean) = 117 | PotentialMatch(graphQLType, javaType, generic, reference, Location.PARAMETER_TYPE, batched) 118 | } 119 | } 120 | class RawClassRequiredForGraphQLMappingException(msg: String): RuntimeException(msg) 121 | } 122 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coxautodev/graphql/tools/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.coxautodev.graphql.tools 2 | 3 | import graphql.language.FieldDefinition 4 | import graphql.language.ListType 5 | import graphql.language.NonNullType 6 | import graphql.language.ObjectTypeDefinition 7 | import graphql.language.Type 8 | import graphql.language.TypeExtensionDefinition 9 | 10 | /** 11 | * @author Andrew Potter 12 | */ 13 | 14 | internal typealias GraphQLRootResolver = GraphQLResolver 15 | internal typealias JavaType = java.lang.reflect.Type 16 | internal typealias GraphQLLangType = graphql.language.Type 17 | 18 | internal fun Type.unwrap(): Type = when(this) { 19 | is NonNullType -> this.type.unwrap() 20 | is ListType -> this.type.unwrap() 21 | else -> this 22 | } 23 | 24 | internal fun ObjectTypeDefinition.getExtendedFieldDefinitions(extensions: List): List { 25 | return this.fieldDefinitions + extensions.filter { it.name == this.name }.flatMap { it.fieldDefinitions } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/Application.kt: -------------------------------------------------------------------------------- 1 | package com.framework 2 | 3 | import org.janusgraph.core.JanusGraph 4 | import org.janusgraph.core.JanusGraphFactory 5 | import org.springframework.boot.SpringApplication 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.ComponentScan 9 | import org.springframework.core.io.ClassPathResource 10 | 11 | 12 | @SpringBootApplication 13 | @ComponentScan(basePackages = ["com"]) 14 | class Application { 15 | 16 | @Bean 17 | fun graph(): JanusGraph = 18 | JanusGraphFactory.open(ClassPathResource("janusgraph-configuration.properties").file.absolutePath) 19 | } 20 | 21 | fun main(args: Array) { 22 | SpringApplication.run(Application::class.java, *args) 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/datamodel/edges/Linked.kt: -------------------------------------------------------------------------------- 1 | package com.framework.datamodel.edges 2 | 3 | 4 | internal interface Linked: Relationship { 5 | 6 | override val inverse: Linked 7 | 8 | override val hops: List> get() = first.hops + last.hops 9 | 10 | val first: Relationship 11 | val last: Relationship<*, TO> 12 | 13 | data class OptionalToOptional( 14 | override val first: Relationship.OneToOne, 15 | override val last: Relationship.OneToOne<*, TO> 16 | ): Linked, Relationship.OptionalToOptional { 17 | 18 | override val inverse: OptionalToOptional get() = 19 | OptionalToOptional(first = last.inverse, last = first.inverse) 20 | } 21 | 22 | data class OptionalToSingle( 23 | override val first: Relationship.OneToSingle, 24 | override val last: Relationship.OneToSingle<*, TO> 25 | ): Linked, Relationship.OptionalToSingle { 26 | 27 | override val inverse: SingleToOptional get() = 28 | SingleToOptional(first = last.inverse, last = first.inverse) 29 | } 30 | 31 | data class SingleToOptional( 32 | override val first: Relationship.SingleToOne, 33 | override val last: Relationship.SingleToOne<*, TO> 34 | ): Linked, Relationship.SingleToOptional { 35 | 36 | override val inverse: OptionalToSingle get() = 37 | OptionalToSingle(first = last.inverse, last = first.inverse) 38 | } 39 | 40 | data class SingleToSingle( 41 | override val first: Relationship.SingleToSingle, 42 | override val last: Relationship.SingleToSingle<*, TO> 43 | ): Linked, Relationship.SingleToSingle { 44 | 45 | override val inverse: SingleToSingle get() = 46 | SingleToSingle(first = last.inverse, last = first.inverse) 47 | } 48 | 49 | data class OptionalToMany( 50 | override val first: Relationship.FromOne, 51 | override val last: Relationship.FromOne<*, TO> 52 | ): Linked, Relationship.OptionalToMany { 53 | 54 | override val inverse: ManyToOptional get() = 55 | ManyToOptional(first = last.inverse, last = first.inverse) 56 | } 57 | 58 | data class SingleToMany( 59 | override val first: Relationship.FromSingle, 60 | override val last: Relationship.FromSingle<*, TO> 61 | ): Linked, Relationship.SingleToMany { 62 | 63 | override val inverse: ManyToSingle get() = 64 | ManyToSingle(first = last.inverse, last = first.inverse) 65 | } 66 | 67 | data class ManyToOptional( 68 | override val first: Relationship.ToOne, 69 | override val last: Relationship.ToOne<*, TO> 70 | ): Linked, Relationship.ManyToOptional { 71 | 72 | override val inverse: OptionalToMany get() = 73 | OptionalToMany(first = last.inverse, last = first.inverse) 74 | } 75 | 76 | data class ManyToSingle( 77 | override val first: Relationship.ToSingle, 78 | override val last: Relationship.ToSingle<*, TO> 79 | ): Linked, Relationship.ManyToSingle { 80 | 81 | override val inverse: SingleToMany get() = 82 | SingleToMany(first = last.inverse, last = first.inverse) 83 | } 84 | 85 | data class ManyToMany( 86 | override val first: Relationship, 87 | override val last: Relationship<*, TO> 88 | ): Linked, Relationship.ManyToMany { 89 | 90 | override val inverse: ManyToMany get() = 91 | ManyToMany(first = last.inverse, last = first.inverse) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/datamodel/edges/Relationship.kt: -------------------------------------------------------------------------------- 1 | package com.framework.datamodel.edges 2 | 3 | interface Relationship { 4 | 5 | val inverse: Relationship 6 | 7 | val hops: List> 8 | 9 | fun to( 10 | next: ManyToMany 11 | ): ManyToMany = 12 | Linked.ManyToMany(first = this, last = next) 13 | 14 | interface FromOne: Relationship { 15 | override val inverse: ToOne 16 | } 17 | 18 | interface FromOptional: FromOne { 19 | 20 | override val inverse: ToOptional 21 | 22 | fun to(next: OptionalToMany): OptionalToMany = 23 | Linked.OptionalToMany(first = this, last = next) 24 | 25 | fun to(next: SingleToMany): OptionalToMany = 26 | Linked.OptionalToMany(first = this, last = next) 27 | } 28 | 29 | interface FromSingle: FromOne { 30 | 31 | override val inverse: ToSingle 32 | 33 | fun to(next: OptionalToMany): OptionalToMany = 34 | Linked.OptionalToMany(first = this, last = next) 35 | 36 | fun to(next: SingleToMany): SingleToMany = 37 | Linked.SingleToMany(first = this, last = next) 38 | } 39 | 40 | 41 | interface FromMany: Relationship { 42 | 43 | override val inverse: ToMany 44 | 45 | fun to(next: OptionalToMany): ManyToMany = 46 | Linked.ManyToMany(first = this, last = next) 47 | 48 | fun to(next: SingleToMany): ManyToMany = 49 | Linked.ManyToMany(first = this, last = next) 50 | } 51 | 52 | interface ToOne: Relationship { 53 | override val inverse: FromOne 54 | } 55 | 56 | interface ToOptional: ToOne { 57 | 58 | override val inverse: FromOptional 59 | 60 | fun to(next: ManyToOptional): ManyToOptional = 61 | Linked.ManyToOptional(first = this, last = next) 62 | 63 | fun to(next: ManyToSingle): ManyToOptional = 64 | Linked.ManyToOptional(first = this, last = next) 65 | } 66 | 67 | interface ToSingle: ToOne { 68 | 69 | override val inverse: FromSingle 70 | 71 | fun to(next: ManyToOptional): ManyToOptional = 72 | Linked.ManyToOptional(first = this, last = next) 73 | 74 | fun to(next: ManyToSingle): ManyToSingle = 75 | Linked.ManyToSingle(first = this, last = next) 76 | } 77 | 78 | interface ToMany: Relationship { 79 | 80 | override val inverse: FromMany 81 | 82 | fun to(next: ManyToOptional): ManyToMany = 83 | Linked.ManyToMany(first = this, last = next) 84 | 85 | fun to(next: ManyToSingle): ManyToMany = 86 | Linked.ManyToMany(first = this, last = next) 87 | } 88 | 89 | interface OneToOne: FromOne, ToOne { 90 | override val inverse: OneToOne 91 | } 92 | 93 | interface OneToOptional: OneToOne, ToOptional { 94 | override val inverse: OptionalToOne 95 | } 96 | 97 | interface OneToSingle: OneToOne, ToSingle { 98 | override val inverse: SingleToOne 99 | } 100 | 101 | interface OptionalToOne: FromOptional, OneToOne { 102 | override val inverse: OneToOptional 103 | } 104 | 105 | interface SingleToOne: FromSingle, OneToOne { 106 | override val inverse: OneToSingle 107 | } 108 | 109 | interface OneToMany: FromOne, ToMany { 110 | override val inverse: ManyToOne 111 | } 112 | 113 | interface ManyToOne: FromMany, ToOne { 114 | override val inverse: OneToMany 115 | } 116 | 117 | interface OptionalToOptional: OptionalToOne, OneToOptional { 118 | 119 | override val inverse: OptionalToOptional 120 | 121 | fun to(next: OptionalToOptional): OptionalToOptional = 122 | Linked.OptionalToOptional(first = this, last = next) 123 | 124 | fun to(next: OptionalToSingle): OptionalToOptional = 125 | Linked.OptionalToOptional(first = this, last = next) 126 | 127 | fun to(next: SingleToOptional): OptionalToOptional = 128 | Linked.OptionalToOptional(first = this, last = next) 129 | 130 | fun to(next: SingleToSingle): OptionalToOptional = 131 | Linked.OptionalToOptional(first = this, last = next) 132 | } 133 | 134 | interface OptionalToSingle: OptionalToOne, OneToSingle { 135 | 136 | override val inverse: SingleToOptional 137 | 138 | fun to(next: OptionalToOptional): OptionalToOptional = 139 | Linked.OptionalToOptional(first = this, last = next) 140 | 141 | fun to(next: OptionalToSingle): OptionalToSingle = 142 | Linked.OptionalToSingle(first = this, last = next) 143 | 144 | fun to(next: SingleToOptional): OptionalToOptional = 145 | Linked.OptionalToOptional(first = this, last = next) 146 | 147 | fun to(next: SingleToSingle): OptionalToSingle = 148 | Linked.OptionalToSingle(first = this, last = next) 149 | } 150 | 151 | interface SingleToOptional: OneToOptional, SingleToOne { 152 | 153 | override val inverse: OptionalToSingle 154 | 155 | fun to(next: OptionalToOptional): OptionalToOptional = 156 | Linked.OptionalToOptional(first = this, last = next) 157 | 158 | fun to(next: OptionalToSingle): OptionalToOptional = 159 | Linked.OptionalToOptional(first = this, last = next) 160 | 161 | fun to(next: SingleToOptional): SingleToOptional = 162 | Linked.SingleToOptional(first = this, last = next) 163 | 164 | fun to(next: SingleToSingle): SingleToOptional = 165 | Linked.SingleToOptional(first = this, last = next) 166 | } 167 | 168 | interface SingleToSingle: OneToSingle, SingleToOne { 169 | 170 | override val inverse: SingleToSingle 171 | 172 | fun to(next: OptionalToOptional): OptionalToOptional = 173 | Linked.OptionalToOptional(first = this, last = next) 174 | 175 | fun to(next: OptionalToSingle): OptionalToSingle = 176 | Linked.OptionalToSingle(first = this, last = next) 177 | 178 | fun to(next: SingleToOptional): SingleToOptional = 179 | Linked.SingleToOptional(first = this, last = next) 180 | 181 | fun to(next: SingleToSingle): SingleToSingle = 182 | Linked.SingleToSingle(first = this, last = next) 183 | } 184 | 185 | interface OptionalToMany: FromOptional, OneToMany { 186 | 187 | override val inverse: ManyToOptional 188 | 189 | fun to(next: OptionalToOptional): OptionalToMany = 190 | Linked.OptionalToMany(first = this, last = next) 191 | 192 | fun to(next: OptionalToSingle): OptionalToMany = 193 | Linked.OptionalToMany(first = this, last = next) 194 | 195 | fun to(next: SingleToOptional): OptionalToMany = 196 | Linked.OptionalToMany(first = this, last = next) 197 | 198 | fun to(next: SingleToSingle): OptionalToMany = 199 | Linked.OptionalToMany(first = this, last = next) 200 | } 201 | 202 | interface SingleToMany: FromSingle, OneToMany { 203 | 204 | override val inverse: ManyToSingle 205 | 206 | fun to(next: OptionalToOptional): OptionalToMany = 207 | Linked.OptionalToMany(first = this, last = next) 208 | 209 | fun to(next: OptionalToSingle): OptionalToMany = 210 | Linked.OptionalToMany(first = this, last = next) 211 | 212 | fun to(next: SingleToOptional): SingleToMany = 213 | Linked.SingleToMany(first = this, last = next) 214 | 215 | fun to(next: SingleToSingle): SingleToMany = 216 | Linked.SingleToMany(first = this, last = next) 217 | } 218 | 219 | interface ManyToOptional: ToOptional, ManyToOne { 220 | 221 | override val inverse: OptionalToMany 222 | 223 | fun to(next: OptionalToOptional): ManyToOptional = 224 | Linked.ManyToOptional(first = this, last = next) 225 | 226 | fun to(next: OptionalToSingle): ManyToOptional = 227 | Linked.ManyToOptional(first = this, last = next) 228 | 229 | fun to(next: SingleToOptional): ManyToOptional = 230 | Linked.ManyToOptional(first = this, last = next) 231 | 232 | fun to(next: SingleToSingle): ManyToOptional = 233 | Linked.ManyToOptional(first = this, last = next) 234 | } 235 | 236 | interface ManyToSingle: ToSingle, ManyToOne { 237 | 238 | override val inverse: SingleToMany 239 | 240 | fun to(next: OptionalToOptional): ManyToOptional = 241 | Linked.ManyToOptional(first = this, last = next) 242 | 243 | fun to(next: OptionalToSingle): ManyToSingle = 244 | Linked.ManyToSingle(first = this, last = next) 245 | 246 | fun to(next: SingleToOptional): ManyToOptional = 247 | Linked.ManyToOptional(first = this, last = next) 248 | 249 | fun to(next: SingleToSingle): ManyToSingle = 250 | Linked.ManyToSingle(first = this, last = next) 251 | } 252 | 253 | interface ManyToMany: FromMany, ToMany { 254 | 255 | override val inverse: ManyToMany 256 | 257 | fun to(next: OptionalToOptional): ManyToMany = 258 | Linked.ManyToMany(first = this, last = next) 259 | 260 | fun to(next: OptionalToSingle): ManyToMany = 261 | Linked.ManyToMany(first = this, last = next) 262 | 263 | fun to(next: SingleToOptional): ManyToMany = 264 | Linked.ManyToMany(first = this, last = next) 265 | 266 | fun to(next: SingleToSingle): ManyToMany = 267 | Linked.ManyToMany(first = this, last = next) 268 | } 269 | 270 | interface Hop: Relationship { 271 | 272 | override val inverse: Hop 273 | 274 | override val hops: List> get() = listOf(this) 275 | 276 | val name: String 277 | 278 | val direction: Direction? 279 | 280 | val isSymmetric: Boolean get() = direction == null 281 | 282 | val type: Type 283 | 284 | enum class Direction { 285 | FORWARD, 286 | BACKWARD; 287 | 288 | val inverse: Direction 289 | get() = when (this) { 290 | FORWARD -> BACKWARD 291 | BACKWARD -> FORWARD 292 | } 293 | } 294 | 295 | enum class Type { 296 | ASYMMETRIC_SINGLE_TO_OPTIONAL, 297 | ASYMMETRIC_OPTIONAL_TO_SINGLE, 298 | ASYMMETRIC_OPTIONAL_TO_OPTIONAL, 299 | ASYMMETRIC_SINGLE_TO_SINGLE, 300 | ASYMMETRIC_MANY_TO_SINGLE, 301 | ASYMMETRIC_MANY_TO_OPTIONAL, 302 | ASYMMETRIC_SINGLE_TO_MANY, 303 | ASYMMETRIC_OPTIONAL_TO_MANY, 304 | ASYMMETRIC_MANY_TO_MANY, 305 | SYMMETRIC_OPTIONAL_TO_OPTIONAL, 306 | SYMMETRIC_SINGLE_TO_SINGLE, 307 | SYMMETRIC_MANY_TO_MANY, 308 | } 309 | 310 | interface FromOne: Hop, Relationship.FromOne { 311 | override val inverse: ToOne 312 | } 313 | 314 | interface ToOne: Hop, Relationship.ToOne { 315 | override val inverse: FromOne 316 | } 317 | 318 | interface FromOptional: FromOne, Relationship.FromOptional { 319 | override val inverse: ToOptional 320 | } 321 | 322 | interface ToOptional: ToOne, Relationship.ToOptional { 323 | override val inverse: FromOptional 324 | } 325 | 326 | interface FromSingle: FromOne, Relationship.FromSingle { 327 | override val inverse: ToSingle 328 | } 329 | 330 | interface ToSingle: ToOne, Relationship.ToSingle { 331 | override val inverse: FromSingle 332 | } 333 | 334 | interface FromMany: Hop, Relationship.FromMany { 335 | override val inverse: ToMany 336 | } 337 | 338 | interface ToMany: Hop, Relationship.ToMany { 339 | override val inverse: FromMany 340 | } 341 | 342 | interface SymmetricOneToOne: FromOne, ToOne, Relationship.OneToOne { 343 | override val inverse: SymmetricOneToOne 344 | } 345 | } 346 | 347 | data class AsymmetricOptionalToOptional( 348 | override val name: String, 349 | override val direction: Relationship.Hop.Direction = Relationship.Hop.Direction.FORWARD 350 | ): 351 | Relationship.Hop.FromOptional, 352 | Relationship.Hop.ToOptional, 353 | Relationship.OptionalToOptional { 354 | 355 | override val inverse: AsymmetricOptionalToOptional get() = 356 | AsymmetricOptionalToOptional(name = name, direction = direction.inverse) 357 | 358 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_OPTIONAL_TO_OPTIONAL 359 | } 360 | 361 | data class AsymmetricOptionalToSingle( 362 | override val name: String, 363 | override val direction: Relationship.Hop.Direction = Relationship.Hop.Direction.FORWARD 364 | ): 365 | Relationship.Hop.FromOptional, 366 | Relationship.Hop.ToSingle, 367 | Relationship.OptionalToSingle { 368 | 369 | override val inverse: AsymmetricSingleToOptional get() = 370 | AsymmetricSingleToOptional(name = name, direction = direction.inverse) 371 | 372 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_OPTIONAL_TO_SINGLE 373 | } 374 | 375 | data class AsymmetricSingleToOptional( 376 | override val name: String, 377 | override val direction: Relationship.Hop.Direction = Relationship.Hop.Direction.FORWARD 378 | ): 379 | Relationship.Hop.FromSingle, 380 | Relationship.Hop.ToOptional, 381 | Relationship.SingleToOptional { 382 | 383 | override val inverse: AsymmetricOptionalToSingle get() = 384 | AsymmetricOptionalToSingle(name = name, direction = direction.inverse) 385 | 386 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_SINGLE_TO_OPTIONAL 387 | } 388 | 389 | data class AsymmetricSingleToSingle( 390 | override val name: String, 391 | override val direction: Relationship.Hop.Direction = Relationship.Hop.Direction.FORWARD 392 | ): 393 | Relationship.Hop.FromSingle, 394 | Relationship.Hop.ToSingle, 395 | Relationship.SingleToSingle { 396 | 397 | override val inverse: AsymmetricSingleToSingle get() = 398 | AsymmetricSingleToSingle(name = name, direction = direction.inverse) 399 | 400 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_SINGLE_TO_SINGLE 401 | } 402 | 403 | data class SymmetricOptionalToOptional( 404 | override val name: String 405 | ) : 406 | Relationship.Hop.SymmetricOneToOne, 407 | Relationship.Hop.FromOptional, 408 | Relationship.Hop.ToOptional, 409 | Relationship.OptionalToOptional { 410 | 411 | override val direction: Relationship.Hop.Direction? get() = null 412 | 413 | override val inverse: SymmetricOptionalToOptional get() = this 414 | 415 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.SYMMETRIC_OPTIONAL_TO_OPTIONAL 416 | } 417 | 418 | data class SymmetricSingleToSingle( 419 | override val name: String 420 | ) : 421 | Relationship.Hop.SymmetricOneToOne, 422 | Relationship.Hop.FromSingle, 423 | Relationship.Hop.ToSingle, 424 | Relationship.SingleToSingle { 425 | 426 | override val direction: Relationship.Hop.Direction? get() = null 427 | 428 | override val inverse: SymmetricSingleToSingle get() = this 429 | 430 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.SYMMETRIC_SINGLE_TO_SINGLE 431 | } 432 | 433 | data class AsymmetricSingleToMany( 434 | override val name: String 435 | ) : 436 | Relationship.Hop.FromSingle, 437 | Relationship.Hop.ToMany, 438 | Relationship.SingleToMany { 439 | 440 | override val direction: Relationship.Hop.Direction? get() = Relationship.Hop.Direction.FORWARD 441 | 442 | override val inverse: AsymmetricManyToSingle get() = 443 | AsymmetricManyToSingle(name = name) 444 | 445 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_SINGLE_TO_MANY 446 | } 447 | 448 | data class AsymmetricOptionalToMany( 449 | override val name: String 450 | ) : 451 | Relationship.Hop.FromOptional, 452 | Relationship.Hop.ToMany, 453 | Relationship.OptionalToMany { 454 | 455 | override val direction: Relationship.Hop.Direction? get() = Relationship.Hop.Direction.FORWARD 456 | 457 | override val inverse: AsymmetricManyToOptional get() = 458 | AsymmetricManyToOptional(name = name) 459 | 460 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_OPTIONAL_TO_MANY 461 | } 462 | 463 | /** 464 | * We restrict creating ManyToOne edge by clients to prevent creation of a 465 | * ManyToOne relationship that is equivalent in meaning to an already defined OneToMany 466 | * relationship, but using a different name. To get a ManyToOne relationship, define it 467 | * as its OneToMany equivalent then get its inverse. 468 | */ 469 | data class AsymmetricManyToOptional internal constructor( 470 | override val name: String 471 | ) : 472 | Relationship.Hop.FromMany, 473 | Relationship.Hop.ToOptional, 474 | Relationship.ManyToOptional { 475 | 476 | override val direction: Relationship.Hop.Direction? get() = Relationship.Hop.Direction.BACKWARD 477 | 478 | override val inverse: AsymmetricOptionalToMany get() = 479 | AsymmetricOptionalToMany(name = name) 480 | 481 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_MANY_TO_OPTIONAL 482 | } 483 | 484 | /** 485 | * We restrict creating ManyToOne edge by clients to prevent creation of a 486 | * ManyToOne relationship that is equivalent in meaning to an already defined OneToMany 487 | * relationship, but using a different name. To get a ManyToOne relationship, define it 488 | * as its OneToMany equivalent then get its inverse. 489 | */ 490 | data class AsymmetricManyToSingle internal constructor( 491 | override val name: String 492 | ) : 493 | Relationship.Hop.FromMany, 494 | Relationship.Hop.ToSingle, 495 | Relationship.ManyToSingle { 496 | 497 | override val direction: Relationship.Hop.Direction? get() = Relationship.Hop.Direction.BACKWARD 498 | 499 | override val inverse: AsymmetricSingleToMany get() = 500 | AsymmetricSingleToMany(name = name) 501 | 502 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_MANY_TO_SINGLE 503 | } 504 | 505 | data class AsymmetricManyToMany( 506 | override val name: String, 507 | override val direction: Relationship.Hop.Direction = Relationship.Hop.Direction.FORWARD 508 | ) : 509 | Relationship.Hop.FromMany, 510 | Relationship.Hop.ToMany, 511 | Relationship.ManyToMany { 512 | 513 | override val inverse: AsymmetricManyToMany get() = 514 | AsymmetricManyToMany(name = name, direction = direction.inverse) 515 | 516 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.ASYMMETRIC_MANY_TO_MANY 517 | } 518 | 519 | data class SymmetricManyToMany( 520 | override val name: String 521 | ) : 522 | Relationship.Hop.FromMany, 523 | Relationship.Hop.ToMany, 524 | Relationship.ManyToMany { 525 | 526 | override val direction: Relationship.Hop.Direction? get() = null 527 | 528 | override val inverse: SymmetricManyToMany get() = this 529 | 530 | override val type: Relationship.Hop.Type get() = Relationship.Hop.Type.SYMMETRIC_MANY_TO_MANY 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/datamodel/edges/Traversal.kt: -------------------------------------------------------------------------------- 1 | package com.framework.datamodel.edges 2 | 3 | 4 | interface Traversal { 5 | 6 | val relationship: Relationship 7 | 8 | interface Bound: Traversal { 9 | val froms: Collection 10 | } 11 | 12 | interface SingleBound: Bound { 13 | val from: FROM 14 | override val froms: Collection get() = listOf(from) 15 | } 16 | 17 | interface MultiBound: Bound 18 | 19 | interface ToOptional: Traversal { 20 | override val relationship: Relationship.ToOptional 21 | fun toMultiBound(): MultiBoundToOptional 22 | } 23 | 24 | interface ToSingle: Traversal { 25 | override val relationship: Relationship.ToSingle 26 | fun toMultiBound(): MultiBoundToSingle 27 | } 28 | 29 | interface ToMany: Traversal { 30 | override val relationship: Relationship.ToMany 31 | fun toMultiBound(): MultiBoundToMany 32 | } 33 | 34 | interface BoundToMany: Bound, ToMany 35 | 36 | interface BoundToOptional: Bound, ToOptional 37 | 38 | interface BoundToSingle: Bound, ToSingle 39 | 40 | data class SingleBoundToMany( 41 | override val from: FROM, 42 | override val relationship: Relationship.ToMany 43 | ): SingleBound, BoundToMany { 44 | override fun toMultiBound() = MultiBoundToMany(froms = listOf(from), relationship = relationship) 45 | } 46 | 47 | data class MultiBoundToMany( 48 | override val froms: Collection, 49 | override val relationship: Relationship.ToMany 50 | ): MultiBound, BoundToMany { 51 | override fun toMultiBound() = this 52 | } 53 | 54 | data class SingleBoundToOptional( 55 | override val from: FROM, 56 | override val relationship: Relationship.ToOptional 57 | ): SingleBound, BoundToOptional { 58 | override fun toMultiBound() = MultiBoundToOptional(froms = listOf(from), relationship = relationship) 59 | } 60 | 61 | data class MultiBoundToOptional( 62 | override val froms: Collection, 63 | override val relationship: Relationship.ToOptional 64 | ): MultiBound, BoundToOptional { 65 | override fun toMultiBound() = this 66 | } 67 | 68 | data class SingleBoundToSingle( 69 | override val from: FROM, 70 | override val relationship: Relationship.ToSingle 71 | ): SingleBound, BoundToSingle { 72 | override fun toMultiBound() = MultiBoundToSingle(froms = listOf(from), relationship = relationship) 73 | } 74 | 75 | data class MultiBoundToSingle( 76 | override val froms: Collection, 77 | override val relationship: Relationship.ToSingle 78 | ): MultiBound, BoundToSingle { 79 | override fun toMultiBound() = this 80 | } 81 | } 82 | 83 | fun FROM.to(relationship: Relationship.ToOptional) = 84 | Traversal.SingleBoundToOptional(from = this, relationship = relationship) 85 | 86 | fun Collection.to(relationship: Relationship.ToOptional) = 87 | Traversal.MultiBoundToOptional(froms = this, relationship = relationship) 88 | 89 | fun FROM.to(relationship: Relationship.ToSingle) = 90 | Traversal.SingleBoundToSingle(from = this, relationship = relationship) 91 | 92 | fun Collection.to(relationship: Relationship.ToSingle) = 93 | Traversal.MultiBoundToSingle(froms = this, relationship = relationship) 94 | 95 | fun FROM.to(relationship: Relationship.ToMany) = 96 | Traversal.SingleBoundToMany(from = this, relationship = relationship) 97 | 98 | fun Collection.to(relationship: Relationship.ToMany) = 99 | Traversal.MultiBoundToMany(froms = this, relationship = relationship) 100 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/datamodel/node/Node.kt: -------------------------------------------------------------------------------- 1 | package com.framework.datamodel.node 2 | 3 | import com.syncleus.ferma.AbstractVertexFrame 4 | 5 | abstract class Node: AbstractVertexFrame() { 6 | 7 | fun id(): Long = getId() 8 | 9 | override fun hashCode(): Int = getId().hashCode() 10 | 11 | override fun equals(other: Any?): Boolean = other != null && other is Node && getId() == other.getId() 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/datamodel/node/NodeQueryResolver.kt: -------------------------------------------------------------------------------- 1 | package com.framework.datamodel.node 2 | 3 | import com.coxautodev.graphql.tools.GraphQLQueryResolver 4 | import com.framework.graphql.TraversalLoader 5 | import com.framework.graphql.fetchOptional 6 | import com.syncleus.ferma.VertexFrame 7 | import graphql.schema.DataFetchingEnvironment 8 | import org.springframework.stereotype.Component 9 | 10 | 11 | @Component 12 | class NodeQueryResolver( 13 | val loader: TraversalLoader 14 | ): GraphQLQueryResolver { 15 | 16 | fun node(id: Long, environment: DataFetchingEnvironment) = 17 | loader.fetchOptional { it.V(id) } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/datamodel/node/NodeTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.framework.datamodel.node 2 | 3 | interface NodeTypeResolver { 4 | 5 | fun getId(node: Node): Long = node.getId() 6 | } 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/graphql/Mutation.kt: -------------------------------------------------------------------------------- 1 | package com.framework.graphql 2 | 3 | import com.syncleus.ferma.FramedGraph 4 | 5 | abstract class Mutation{ 6 | 7 | open fun checkPermissions(graph: FramedGraph): Boolean = true 8 | 9 | abstract fun run(graph: FramedGraph): T 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/graphql/TraversalDataLoader.kt: -------------------------------------------------------------------------------- 1 | package com.framework.graphql 2 | 3 | import com.framework.datamodel.edges.Traversal 4 | import com.framework.gremlin.asGremlin 5 | import com.syncleus.ferma.FramedGraph 6 | import com.syncleus.ferma.Traversable 7 | import com.syncleus.ferma.VertexFrame 8 | import kotlinx.coroutines.experimental.async 9 | import kotlinx.coroutines.experimental.future.future 10 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal 11 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource 12 | import org.springframework.stereotype.Component 13 | import java.util.concurrent.CompletableFuture 14 | 15 | /** 16 | * Turning off the traversal loader below because graphql-tools does not yet support 17 | * request-scoped components. Batched data loading requires the batches to be request scoped. 18 | */ 19 | //@Component 20 | //class TraversalLoader( 21 | // graph: FramedGraph 22 | //): DataLoader>({ keys -> future { 23 | // keys.map { key -> async { 24 | // graph.traverse> { key(it) }.toList(VertexFrame::class.java) 25 | // } }.map { 26 | // it.await() 27 | // } 28 | //} }, DataLoaderOptions()) { 29 | // 30 | // fun load(key: (GraphTraversalSource) -> GraphTraversal<*, *>): CompletableFuture> = 31 | // load(FunctionTraversalProvider(key)) 32 | // 33 | // fun load(key: Traversal.Bound): CompletableFuture> = 34 | // load(BoundTraversalProvider(key)) 35 | //} 36 | 37 | @Component 38 | class TraversalLoader( 39 | private val graph: FramedGraph 40 | ) { 41 | private fun load(traversalProvider: TraversalProvider): CompletableFuture> { 42 | return future { async { 43 | graph.traverse> { traversalProvider(it) }.toList(VertexFrame::class.java) 44 | }.await() } 45 | } 46 | 47 | fun load(key: (GraphTraversalSource) -> GraphTraversal<*, *>): CompletableFuture> = 48 | load(FunctionTraversalProvider(key)) 49 | 50 | fun load(key: Traversal.Bound): CompletableFuture> = 51 | load(BoundTraversalProvider(key)) 52 | } 53 | 54 | inline fun TraversalLoader.fetchOptional( 55 | noinline traversal: (GraphTraversalSource) -> GraphTraversal<*, *> 56 | ): CompletableFuture = 57 | load(traversal).thenApplyAsync { 58 | it.filterIsInstance(T::class.java).optional() 59 | } 60 | 61 | inline fun TraversalLoader.fetchSingle( 62 | noinline traversal: (GraphTraversalSource) -> GraphTraversal<*, *> 63 | ): CompletableFuture = 64 | load(traversal).thenApplyAsync { 65 | it.filterIsInstance(T::class.java).single() 66 | } 67 | 68 | inline fun TraversalLoader.fetchMany( 69 | noinline traversal: (GraphTraversalSource) -> GraphTraversal<*, *> 70 | ): CompletableFuture> = 71 | load(traversal).thenApplyAsync { 72 | it.filterIsInstance(T::class.java) 73 | } 74 | 75 | inline fun TraversalLoader.fetch( 76 | traversal: Traversal.BoundToOptional 77 | ): CompletableFuture = 78 | load(traversal).thenApplyAsync { 79 | it.filterIsInstance(T::class.java).optional() 80 | } 81 | 82 | inline fun TraversalLoader.fetch( 83 | traversal: Traversal.BoundToSingle 84 | ): CompletableFuture = 85 | load(traversal).thenApplyAsync { 86 | it.filterIsInstance(T::class.java).single() 87 | } 88 | 89 | inline fun TraversalLoader.fetch( 90 | traversal: Traversal.BoundToMany 91 | ): CompletableFuture> = 92 | load(traversal).thenApplyAsync { 93 | it.filterIsInstance(T::class.java) 94 | } 95 | 96 | fun Iterable.optional(): T? = 97 | if (!iterator().hasNext()) null else single() 98 | 99 | /** 100 | * Implementing TraversalProvider as data classes enable equality checks which allow TraversalProviders 101 | * to be cached (if a TraversalLoader has caching capabilities. 102 | */ 103 | 104 | interface TraversalProvider { 105 | operator fun invoke(source: GraphTraversalSource): GraphTraversal<*, *> 106 | } 107 | 108 | data class BoundTraversalProvider(private val traversal: Traversal.Bound): TraversalProvider { 109 | override fun invoke(source: GraphTraversalSource): GraphTraversal<*, *> = traversal.asGremlin(source) 110 | } 111 | 112 | data class FunctionTraversalProvider(private val traversal: (GraphTraversalSource) -> GraphTraversal<*, *>): TraversalProvider { 113 | override fun invoke(source: GraphTraversalSource) = traversal(source) 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/com/framework/gremlin/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.framework.gremlin 2 | 3 | import com.framework.datamodel.edges.Traversal 4 | import com.framework.graphql.Mutation 5 | import com.framework.graphql.optional 6 | import com.syncleus.ferma.FramedGraph 7 | import com.syncleus.ferma.Traversable 8 | import com.syncleus.ferma.VertexFrame 9 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal 10 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource 11 | import org.apache.tinkerpop.gremlin.structure.Vertex 12 | 13 | 14 | fun Traversal.Bound.asGremlin( 15 | source: GraphTraversalSource 16 | ): GraphTraversal = relationship.hops.fold( 17 | initial = source.V(froms.map { from -> from.getId() }), 18 | operation = { traversal, hop -> traversal.out(hop.name) }) 19 | 20 | fun GraphTraversalSource.vertexIds(vertexIds: Collection): GraphTraversal 21 | = V(*vertexIds.toTypedArray()) 22 | 23 | inline fun FramedGraph.insert(): T = addFramedVertex(T::class.java) 24 | 25 | inline fun FramedGraph.fetchOptional( 26 | noinline traversal: (GraphTraversalSource) -> GraphTraversal<*, *> 27 | ): T? = traverse> { traversal(it) }.toList(T::class.java).optional() 28 | 29 | inline fun FramedGraph.fetchSingle( 30 | noinline traversal: (GraphTraversalSource) -> GraphTraversal<*, *> 31 | ): T = traverse> { traversal(it) }.toList(T::class.java).single() 32 | 33 | inline fun FramedGraph.fetchMany( 34 | noinline traversal: (GraphTraversalSource) -> GraphTraversal<*, *> 35 | ): List = traverse> { traversal(it) }.toList(T::class.java) 36 | 37 | 38 | fun FramedGraph.mutate(mutation: Mutation): T { 39 | if (!mutation.checkPermissions(this)) { 40 | throw Exception("Mutation permissions failed") 41 | } 42 | val result = mutation.run(this) 43 | tx().commit() 44 | return result 45 | } 46 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/StarwarsGraph.kt: -------------------------------------------------------------------------------- 1 | package com.starwars 2 | 3 | import com.framework.gremlin.insert 4 | import com.starwars.character.Character 5 | import com.starwars.character.addFriends 6 | import com.starwars.character.setAppearsIn 7 | import com.starwars.character.setFriends 8 | import com.starwars.droid.Droid 9 | import com.starwars.episode.Episode 10 | import com.starwars.human.Human 11 | import com.syncleus.ferma.DelegatingFramedGraph 12 | import com.syncleus.ferma.FramedGraph 13 | import org.janusgraph.core.JanusGraph 14 | import org.springframework.stereotype.Component 15 | 16 | @Component 17 | class StarwarsGraph(graph: JanusGraph): DelegatingFramedGraph( 18 | graph, 19 | true, 20 | setOf( 21 | Character::class.java, 22 | Droid::class.java, 23 | Human::class.java, 24 | Episode::class.java)) { 25 | init { 26 | loadStarwars() 27 | } 28 | } 29 | 30 | private fun FramedGraph.loadStarwars() { 31 | val newHope = insert() 32 | newHope.setName("New Hope") 33 | 34 | val jedi = insert() 35 | jedi.setName("Return of the Jedi") 36 | 37 | val empire = insert() 38 | empire.setName("Empire Strikes Back") 39 | 40 | val lukeSkywalker = insert() 41 | lukeSkywalker.setName("Luke Skywalker") 42 | lukeSkywalker.setHomePlanet("Tatooine") 43 | lukeSkywalker.setAppearsIn(newHope, jedi, empire) 44 | 45 | val darthVader = insert() 46 | darthVader.setName("Darth Vader") 47 | darthVader.setHomePlanet("Tatooine") 48 | darthVader.setAppearsIn(newHope, jedi, empire) 49 | 50 | val hanSolo = insert() 51 | hanSolo.setName("Han Solo") 52 | hanSolo.setAppearsIn(newHope, jedi, empire) 53 | 54 | val leiaOrgana = insert() 55 | leiaOrgana.setName("Leia Organa") 56 | leiaOrgana.setHomePlanet("Alderaan") 57 | leiaOrgana.setAppearsIn(newHope, jedi, empire) 58 | 59 | val wilhuffTarkin = insert() 60 | wilhuffTarkin.setName("Wilhuff Tarkin") 61 | wilhuffTarkin.setAppearsIn(newHope) 62 | 63 | val c3po = insert() 64 | c3po.setName("C-3PO") 65 | c3po.setAppearsIn(newHope, jedi, empire) 66 | c3po.setPrimaryFunction("Protocol") 67 | 68 | val aretoo = insert() 69 | aretoo.setName("R2-D2") 70 | aretoo.setAppearsIn(newHope, jedi, empire) 71 | aretoo.setPrimaryFunction("Astromech") 72 | 73 | lukeSkywalker.setFriends(hanSolo, leiaOrgana, c3po, aretoo) 74 | darthVader.setFriends(wilhuffTarkin) 75 | hanSolo.setFriends(lukeSkywalker, leiaOrgana, aretoo) 76 | leiaOrgana.setFriends(lukeSkywalker, hanSolo, c3po, aretoo) 77 | wilhuffTarkin.setFriends(darthVader) 78 | 79 | c3po.addFriends(lukeSkywalker, hanSolo, leiaOrgana, aretoo) 80 | aretoo.addFriends(lukeSkywalker, hanSolo, leiaOrgana) 81 | 82 | tx().commit() 83 | println("Loaded Starwars Data") 84 | } 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/character/Character.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.character 2 | 3 | 4 | import com.framework.datamodel.edges.Relationship 5 | import com.framework.datamodel.edges.to 6 | import com.framework.datamodel.node.Node 7 | import com.starwars.episode.Episode 8 | import com.syncleus.ferma.annotations.Adjacency 9 | import com.syncleus.ferma.annotations.Incidence 10 | import com.syncleus.ferma.annotations.Property 11 | 12 | abstract class Character: Node() { 13 | 14 | @Property("name") 15 | abstract fun getName(): String 16 | 17 | @Property("name") 18 | abstract fun setName(name: String) 19 | 20 | val toAppearsIn get() = to(appearsIn) 21 | 22 | val toFriends get() = to(friends) 23 | 24 | val toSecondDegreeFriends get() = to(secondDegreeFriends) 25 | 26 | @Incidence(label = "friends") 27 | abstract fun addFriend(friend: Character) 28 | 29 | @Adjacency(label = "friends") 30 | abstract fun setFriends(friends: Set) 31 | 32 | @Incidence(label = "appearsIn") 33 | abstract fun addAppearsIn(appearsIn: Episode) 34 | 35 | @Adjacency(label = "appearsIn") 36 | abstract fun setAppearsIn(appearsIn: Set) 37 | 38 | companion object { 39 | val friends = Relationship.AsymmetricManyToMany(name = "friends") 40 | val secondDegreeFriends = friends.to(friends) 41 | val appearsIn = Relationship.AsymmetricManyToMany(name = "appearsIn") 42 | } 43 | } 44 | 45 | fun Character.setAppearsIn(appearsIn: Iterable) = setAppearsIn(appearsIn.toSet()) 46 | fun Character.setAppearsIn(vararg appearsIn: Episode) = setAppearsIn(appearsIn.toSet()) 47 | fun Character.addAppearsIn(vararg appearsIn: Episode) = appearsIn.forEach { addAppearsIn(it) } 48 | fun Character.addAppearsIn(appearsIn: Set) = appearsIn.forEach { addAppearsIn(it) } 49 | 50 | fun Character.setFriends(friends: Iterable) = setFriends(friends.toSet()) 51 | fun Character.setFriends(vararg friends: Character) = setFriends(friends.toSet()) 52 | fun Character.addFriends(vararg friends: Character) = friends.forEach { addFriend(it) } 53 | fun Character.addFriends(friends: Set) = friends.forEach { addFriend(it) } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/character/CharacterQueryResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.character 2 | 3 | import com.coxautodev.graphql.tools.GraphQLQueryResolver 4 | import com.framework.graphql.TraversalLoader 5 | import com.framework.graphql.fetchOptional 6 | import com.framework.graphql.fetchSingle 7 | import graphql.schema.DataFetchingEnvironment 8 | import org.springframework.stereotype.Component 9 | 10 | @Component 11 | class CharacterQueryResolver( 12 | val loader: TraversalLoader 13 | ): GraphQLQueryResolver { 14 | 15 | fun hero(environment: DataFetchingEnvironment) = 16 | loader.fetchSingle { it.V().has("name", "Luke Skywalker") } 17 | 18 | fun character(name: String, environment: DataFetchingEnvironment) = 19 | loader.fetchOptional { it.V().has("name", name) } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/character/CharacterTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.character 2 | 3 | import com.framework.datamodel.node.NodeTypeResolver 4 | import com.framework.graphql.TraversalLoader 5 | import com.framework.graphql.fetch 6 | import com.framework.graphql.fetchMany 7 | import com.framework.gremlin.asGremlin 8 | 9 | interface CharacterTypeResolver: NodeTypeResolver { 10 | 11 | val loader: TraversalLoader 12 | 13 | fun getName(character: Character) = character.getName() 14 | fun getAppearsIn(character: Character) = loader.fetch(character.toAppearsIn) 15 | fun getFriends(character: Character) = loader.fetch(character.toFriends) 16 | 17 | fun getSecondDegreeFriends(character: Character, limit: Int?) = loader.fetchMany { 18 | val secondDegree = character.toSecondDegreeFriends.asGremlin(it) 19 | .dedup() 20 | .filter { it.get().id() != character.getId() } 21 | if (limit == null) secondDegree else secondDegree.limit(limit.toLong()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/droid/Droid.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.droid 2 | 3 | import com.starwars.character.Character 4 | import com.syncleus.ferma.annotations.Property 5 | 6 | abstract class Droid: Character() { 7 | 8 | @Property("primaryFunction") 9 | abstract fun getPrimaryFunction(): String 10 | 11 | @Property("primaryFunction") 12 | abstract fun setPrimaryFunction(primaryFunction: String) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/droid/DroidMutationResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.droid 2 | 3 | import com.coxautodev.graphql.tools.GraphQLMutationResolver 4 | import com.framework.graphql.Mutation 5 | import com.framework.gremlin.fetchMany 6 | import com.framework.gremlin.insert 7 | import com.framework.gremlin.mutate 8 | import com.framework.gremlin.vertexIds 9 | import com.starwars.character.Character 10 | import com.starwars.character.setAppearsIn 11 | import com.starwars.character.setFriends 12 | import com.syncleus.ferma.FramedGraph 13 | import graphql.schema.DataFetchingEnvironment 14 | import org.springframework.stereotype.Component 15 | 16 | @Component 17 | class DroidMutationResolver( 18 | val graph: FramedGraph 19 | ) : GraphQLMutationResolver { 20 | 21 | fun createDroid( 22 | name: String, 23 | primaryFunction: String, 24 | friendIds: Set, 25 | appearsInIds: Set, 26 | environment: DataFetchingEnvironment) = graph.mutate(object : Mutation() { 27 | 28 | override fun checkPermissions(graph: FramedGraph) = true 29 | 30 | override fun run(graph: FramedGraph): Droid { 31 | val friends = graph.fetchMany { it.vertexIds(friendIds) } 32 | val droid = graph.insert() 33 | droid.setName(name) 34 | droid.setPrimaryFunction(primaryFunction) 35 | droid.setAppearsIn(graph.fetchMany { it.vertexIds(appearsInIds) }) 36 | droid.setFriends(friends) 37 | friends.forEach { it.addFriend(droid) } 38 | return droid 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/droid/DroidQueryResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.droid 2 | 3 | import com.coxautodev.graphql.tools.GraphQLQueryResolver 4 | import com.framework.graphql.TraversalLoader 5 | import com.framework.graphql.fetchOptional 6 | import graphql.schema.DataFetchingEnvironment 7 | import org.springframework.stereotype.Component 8 | 9 | 10 | @Component 11 | class DroidQueryResolver( 12 | val loader: TraversalLoader 13 | ): GraphQLQueryResolver { 14 | 15 | fun droid(name: String, environment: DataFetchingEnvironment) = 16 | loader.fetchOptional { it.V().has("name", name) } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/droid/DroidTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.droid 2 | 3 | import com.coxautodev.graphql.tools.GraphQLResolver 4 | import com.framework.graphql.TraversalLoader 5 | import com.starwars.character.CharacterTypeResolver 6 | import org.springframework.stereotype.Component 7 | 8 | @Component 9 | class DroidTypeResolver( 10 | override val loader: TraversalLoader 11 | ): CharacterTypeResolver, GraphQLResolver { 12 | 13 | fun getPrimaryFunction(droid: Droid) = droid.getPrimaryFunction() 14 | 15 | // These redundant overrides are necessary for graphql.tools 16 | fun getId(node: Droid) = super.getId(node) 17 | fun getName(character: Droid) = super.getName(character) 18 | fun getAppearsIn(character: Droid) = super.getAppearsIn(character) 19 | fun getFriends(character: Droid) = super.getFriends(character) 20 | fun getSecondDegreeFriends(character: Droid, limit: Int?) = super.getSecondDegreeFriends(character, limit) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/episode/Episode.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.episode 2 | 3 | import com.framework.datamodel.node.Node 4 | import com.syncleus.ferma.annotations.Property 5 | 6 | abstract class Episode: Node() { 7 | 8 | @Property("name") 9 | abstract fun getName(): String 10 | 11 | @Property("name") 12 | abstract fun setName(name: String) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/episode/EpisodeTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.episode 2 | 3 | import com.coxautodev.graphql.tools.GraphQLResolver 4 | import com.framework.datamodel.node.Node 5 | import com.framework.datamodel.node.NodeTypeResolver 6 | import com.framework.graphql.TraversalLoader 7 | import com.starwars.droid.Droid 8 | import com.starwars.human.Human 9 | import org.springframework.stereotype.Component 10 | 11 | @Component 12 | class EpisodeTypeResolver: NodeTypeResolver, GraphQLResolver { 13 | fun getName(episode: Episode) = episode.getName() 14 | 15 | // This redundant override is necessary for graphql.tools 16 | fun getId(node: Episode) = super.getId(node) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/human/Human.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.human 2 | 3 | import com.starwars.character.Character 4 | import com.syncleus.ferma.annotations.Property 5 | 6 | abstract class Human: Character() { 7 | 8 | @Property("homePlanet") 9 | abstract fun getHomePlanet(): String? 10 | 11 | @Property("homePlanet") 12 | abstract fun setHomePlanet(homePlanet: String?) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/human/HumanQueryResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.human 2 | 3 | import com.coxautodev.graphql.tools.GraphQLQueryResolver 4 | import com.framework.graphql.TraversalLoader 5 | import com.framework.graphql.fetchOptional 6 | import graphql.schema.DataFetchingEnvironment 7 | import org.springframework.stereotype.Component 8 | 9 | @Component 10 | class HumanQueryResolver( 11 | val loader: TraversalLoader 12 | ) : GraphQLQueryResolver { 13 | 14 | fun human(name: String, environment: DataFetchingEnvironment) = 15 | loader.fetchOptional { it.V().has("name", name) } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/starwars/human/HumanTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.starwars.human 2 | 3 | import com.coxautodev.graphql.tools.GraphQLResolver 4 | import com.framework.datamodel.node.Node 5 | import com.framework.graphql.TraversalLoader 6 | import com.framework.graphql.fetch 7 | import com.starwars.character.Character 8 | import com.starwars.character.CharacterTypeResolver 9 | import com.starwars.droid.Droid 10 | import org.springframework.stereotype.Component 11 | 12 | @Component 13 | class HumanTypeResolver( 14 | override val loader: TraversalLoader 15 | ): CharacterTypeResolver, GraphQLResolver { 16 | 17 | fun getHomePlanet(human: Human) = human.getHomePlanet() 18 | 19 | // These redundant overrides are necessary for graphql.tools 20 | fun getId(node: Human) = super.getId(node) 21 | fun getName(character: Human) = super.getName(character) 22 | fun getAppearsIn(character: Human) = super.getAppearsIn(character) 23 | fun getFriends(character: Human) = super.getFriends(character) 24 | fun getSecondDegreeFriends(character: Human, limit: Int?) = super.getSecondDegreeFriends(character, limit) 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=5000 2 | spring.groovy.template.check-template-location=false 3 | -------------------------------------------------------------------------------- /src/main/resources/graphqls/starwars.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | 3 | # Find node by id 4 | node(id: ID!): Node 5 | 6 | # Hero of the Star wars saga 7 | hero: Character! 8 | 9 | # Find human by name 10 | human(name: String!): Human 11 | 12 | # Find droid by its name 13 | droid(name: String!): Droid 14 | 15 | # Find character by name 16 | character(name: String!): Character 17 | } 18 | 19 | type Mutation { 20 | 21 | # Create a new droid 22 | createDroid(name: String!, primaryFunction: String!, friendIds: [ID!]!, appearsInIds: [ID!]!): Droid! 23 | } 24 | 25 | interface Node { 26 | 27 | # The id of the node 28 | id: ID! 29 | } 30 | 31 | # A character in the Star Wars Trilogy 32 | interface Character { 33 | 34 | # The id of the character 35 | id: ID! 36 | 37 | # The name of the character 38 | name: String! 39 | 40 | # The friends of the character, or an empty list if they have none 41 | friends: [Character!]! 42 | 43 | # Which movies they appear in 44 | appearsIn: [Episode!]! 45 | 46 | # The friends of the droid, or an empty list if they have none 47 | secondDegreeFriends(limit: Int): [Character!]! 48 | } 49 | 50 | # One of the films in the Star Wars Trilogy 51 | type Episode implements Node { 52 | 53 | # The id of the Episode 54 | id: ID! 55 | 56 | # The name of the episode 57 | name: String! 58 | } 59 | 60 | # A humanoid creature in the Star Wars universe 61 | type Human implements Node, Character { 62 | 63 | # The id of the human 64 | id: ID! 65 | 66 | # The name of the human 67 | name: String! 68 | 69 | # The friends of the human, or an empty list if they have none 70 | friends: [Character!]! 71 | 72 | # Which movies they appear in 73 | appearsIn: [Episode!]! 74 | 75 | # The home planet of the human, or null if unknown 76 | homePlanet: String 77 | 78 | # The friends of the human's friends 79 | secondDegreeFriends(limit: Int): [Character!]! 80 | } 81 | 82 | # A mechanical creature in the Star Wars universe 83 | type Droid implements Node, Character { 84 | 85 | # The id of the droid 86 | id: ID! 87 | 88 | # The name of the droid 89 | name: String! 90 | 91 | # The friends of the droid, or an empty list if they have none 92 | friends: [Character!]! 93 | 94 | # Which movies they appear in 95 | appearsIn: [Episode!]! 96 | 97 | # The primary function of the droid 98 | primaryFunction: String! 99 | 100 | # The friends of the droid's friends 101 | secondDegreeFriends(limit: Int): [Character!]! 102 | } 103 | -------------------------------------------------------------------------------- /src/main/resources/janusgraph-configuration.properties: -------------------------------------------------------------------------------- 1 | storage.backend=inmemory 2 | -------------------------------------------------------------------------------- /src/main/resources/static/graphiql.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GraphiQL 6 | 7 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
Loading...
30 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/test/kotlin/com/starwars/GraphTests.kt: -------------------------------------------------------------------------------- 1 | package com.starwars 2 | 3 | import com.starwars.character.Character 4 | import com.starwars.human.Human 5 | import com.syncleus.ferma.Traversable 6 | import org.assertj.core.api.Assertions 7 | import org.janusgraph.core.JanusGraphFactory 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.springframework.core.io.ClassPathResource 11 | 12 | class GraphTests { 13 | 14 | lateinit var graph: StarwarsGraph 15 | 16 | @Before 17 | fun setup() { 18 | val configuration = ClassPathResource("janusgraph-configuration.properties").file.absolutePath 19 | val janus = JanusGraphFactory.open(configuration) 20 | graph = StarwarsGraph(janus) 21 | } 22 | 23 | @Test 24 | fun contextLoads() { 25 | val luke = graph.traverse> { it.V().has("name", "Luke Skywalker") }.next(Human::class.java) 26 | Assertions.assertThat(luke.getName()).isEqualTo("Luke Skywalker") 27 | Assertions.assertThat(luke.getHomePlanet()).isEqualTo("Tatooine") 28 | } 29 | } 30 | --------------------------------------------------------------------------------