Repository: jcrygier/graphql-jpa Branch: master Commit: 0aab3d31074d Files: 44 Total size: 101.5 KB Directory structure: gitextract_gdex4ibc/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bintray.json ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src/ ├── main/ │ └── java/ │ └── org/ │ └── crygier/ │ └── graphql/ │ ├── AttributeMapper.java │ ├── ExtendedJpaDataFetcher.java │ ├── GraphQLExecutor.java │ ├── GraphQLSchemaBuilder.java │ ├── IdentityCoercing.java │ ├── JavaScalars.java │ ├── JpaDataFetcher.java │ └── annotation/ │ ├── GraphQLIgnore.java │ └── SchemaDocumentation.java └── test/ ├── groovy/ │ └── org/ │ └── crygier/ │ └── graphql/ │ ├── EmbeddedQueryExecutorTest.groovy │ ├── EmbeddedSchemaBuildTest.groovy │ ├── GraphQlController.groovy │ ├── JavaScalarsTest.groovy │ ├── MutableQueryExecutorTest.groovy │ ├── MutableSchemaBuildTest.groovy │ ├── StarwarsQueryExecutorTest.groovy │ ├── StarwarsSchemaBuildTest.groovy │ ├── TestApplication.groovy │ ├── ThingQueryExecutorTest.groovy │ └── model/ │ ├── collections/ │ │ └── CollectionTest.java │ ├── embeddings/ │ │ ├── EmbeddingId.java │ │ └── EmbeddingTest.java │ ├── starwars/ │ │ ├── Character.groovy │ │ ├── CodeList.groovy │ │ ├── Droid.groovy │ │ ├── Episode.java │ │ ├── Human.groovy │ │ └── Spaceship.groovy │ ├── users/ │ │ ├── DateAndUser.groovy │ │ └── User.groovy │ └── uuid/ │ └── Thing.groovy └── resources/ ├── application.yaml ├── data.sql └── static/ └── index.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .gradle/ .idea/ bin/ build/ *.iml .classpath .project .settings/ classes/ out/ ================================================ FILE: .travis.yml ================================================ language: java jdk: - oraclejdk8 deploy: provider: bintray file: bintray.json user: jcrygier key: secure: obNe/VH/vvGr3nSpUQW7+E0XfQAA8zvkuFOM86AfRs51pxblIgLnBkGua9iuRZ8iV6s/275j/SgVV4vkoq1Bkt1ndnmEcsn0XbRuRhJH7yxDdIIclLP/kCAgRFh08ADKD2k51HsqCIjiiSs+B8V0QFQx9pcMtRMchqq/63H1I/TGHUHEpVLh/2+HkTkqFe/0LavxPi1jMt5qkqsPzyCwvE7oM5rzqKUmAbCh66yhi1ao0DkwJybtIq0yjgfKHVKydAr2O0So8kYRkRnddBxi1NkTrbcXm9kwcrM+bmS4S7eMgRWiq/oNYBD0sefYP5NySsDh+phzjIqGFxvTQvp/EfzKlupR2mogK3GjHmuyl0tII4qjrxEnlT6Pj08UvUb2jloHvckdqpj0Gz8f3Iq3/HsxVRXBMshm11dEFXqHIs6UbGQFtf5TkgP88iPt3kfh+0yAWh5EVp+YAG6HnkrTure2YC0JgiRmYHFD0lYxFQ0LoGteECNg1NpMu1lU/f0EaEy98XX+aCjW5KPSyWNEpTXo74IZsjIZEqN1mbfxvtyKc1p1ANK1oQW23FeAI0ZjnCT8FEntk1UJIZqazw2QQvCyYVawESgqFCzkpN4D6uJjU15xQKQos7tK9f1Lm6Xl47PcaA82iFYFxQji3VdC1a+vlCXWPP+XKua2OPhuAoQ= ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 John Crygier and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ GraphQL for JPA =============== If you're already using JPA, then you already have a schema defined...don't define it again just for GraphQL! This is a simple project to extend [graphql-java](https://github.com/andimarek/graphql-java) and have it derive the schema from a JPA model. It also implements an execution platform to generate and run JPA queries based on GraphQL queries. While limited, there is a lot of power bundled within. This project takes a somewhat opinionated view of GraphQL, by introducing things like Pagination, Aggregations, and Sorting. This project is intended on depending on very little: graphql-java, and some javax annotation packages. The tests depend on Spring (with Hibernate for JPA), and Spock for testing. These tests are a good illustration of how this library might be used, but any stack (with JPA) should be able to be utilized. Schema Generation ----------------- Using a JPA Entity Manager, the models are introspected, and a GraphQL Schema is built. With this GraphQL schema, graphql-java does most of the work, except for querying. Schema Documentation -------------------- A major part of GraphQL is the ability to have a well documented schema. This project takes advantage of this, and produces descriptions for each Entity in the schema. For the built in types (e.g. PaginationObject) these are rather hard-coded without much control from the end user. However, for each Entity / Member that is in your JPA schema, you can document what it's for. These descriptions are controlled by the `@SchemaDocumentation` attribute on either a class level, or a field level of your model. These descriptions will show up in the GraphiQL browser automatically, and generally helps when providing an API to your end-users. See the GraphiQL section below for more details. Pagination ---------- GraphQL does not specify any language or idioms for performing Pagination. Therefore, this library takes an opinionated view, similar to that of Spring. Each model (say Human or Droid - see tests) will have two representations in the generated schema: - One that models the Entities directly (Human or Droid) - One that wraps the Entity in a page request (HumanConnection or DroidConnection) This allows you to query for the "Page" version of any Entity, and return metadata (like total count) alongside of the actual requested data. For example: { HumanConnection(paginationRequest: { page: 1, size: 2 }) { totalPages totalElements content { name } } } Will return: { HumanConnection: { totalPages: 3, totalElements: 5, content: [ { name: 'Luke Skywalker' }, { name: 'Darth Vader' } ] } } Of course, an extra query is needed to get the total elements, so if you have not requested 'totalPages' or 'totalElements' this query will not be executed. NOTE: The "Connection" name is used here for further extension (Aggregations, etc...). The name is borrowed from suggestions by Facebook developers: https://github.com/facebook/graphql/issues/4 Aggregations ------------ Not yet implemented, but will be similar to Pagination Sorting ------- Sorting is supported on any field. Simply pass in an 'orderBy' argument with the value of ASC or DESC. Here's an example of sorting by name for Human objects: { Human { name(orderBy: DESC) homePlanet } } Query Injectors --------------- Not yet implemented. Main use case would be to intercept query execution for security purposes. GraphiQL -------- GraphiQL (https://github.com/graphql/graphiql) has been introduced for simple testing (in the test package, as I don't want to assume your web stack). Simply launch TestApplication as a Java Application, and navigate to http://localhost:8080/ to launch. You will notice a 'Docs' button at the upper right, that when expanded will show you the running schema (Star Wars in this demo). You can enter GraphQL queries in the left pannel, and hit the run button, and the results should come back in the right panel. If your query has variables, there is a minimized panel at the bottom left. Simply click on this to expand, and type in your variables as a JSON string (don't forget to quote the keys!). Enjoy! ================================================ FILE: bintray.json ================================================ { "package": { "name": "GraphQL-JPA", "repo": "maven", "subject": "jcrygier" }, "version": { "name": "0.6" }, "files": [ {"includePattern": "build/libs/(.*)", "excludePattern": ".*/do-not-deploy/.*", "uploadPattern": "com/crygier/graphql-jpa/0.6/$1"} ], "publish": true } ================================================ FILE: build.gradle ================================================ apply plugin: 'groovy' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'maven' apply plugin: 'maven-publish' group = 'com.crygier' version = '0.6' jar { baseName = 'graphql-jpa' version = project.version } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() maven { url "http://dl.bintray.com/andimarek/graphql-java" } } configurations { provided compile.extendsFrom provided } dependencies { compile 'com.graphql-java:graphql-java:4.2' compile 'javax.transaction:javax.transaction-api:1.2' provided 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' testCompile "org.springframework.boot:spring-boot-starter:1.5.1.RELEASE" testCompile "org.springframework.boot:spring-boot-starter-test:1.5.1.RELEASE" testCompile "org.springframework.boot:spring-boot-starter-data-jpa:1.5.1.RELEASE" testCompile "org.springframework.boot:spring-boot-starter-web:1.5.1.RELEASE" testCompile 'junit:junit:4.11' testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' testCompile 'org.spockframework:spock-spring:1.0-groovy-2.4' testCompile 'org.codehaus.groovy:groovy-all:2.4.4' testCompile 'cglib:cglib-nodep:3.1' testCompile 'org.objenesis:objenesis:2.1' testRuntime "com.h2database:h2:1.4.190" //testRuntime 'org.hibernate:hibernate-validator:4.3.0.Final' } publishing { publications { MyPublication(MavenPublication) { from components.java groupId 'com.crygier' artifactId 'graphql-jpa' version project.version artifact sourcesJar } } } model { tasks.generatePomFileForMyPublicationPublication { destination = file("$buildDir/libs/${project.name}-${version}.pom") } tasks.assemble { dependsOn tasks.generatePomFileForMyPublicationPublication } } task sourcesJar(type: Jar, dependsOn: classes) { classifier = 'sources' from sourceSets.main.allSource } artifacts { archives sourcesJar } eclipse { classpath { containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' } } task wrapper(type: Wrapper) { gradleVersion = '2.3' } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Sep 15 19:06:52 CDT 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: src/main/java/org/crygier/graphql/AttributeMapper.java ================================================ package org.crygier.graphql; import graphql.schema.GraphQLType; import java.util.Optional; /** * (Functional) Interface to map Classes to GraphQLTypes. */ @FunctionalInterface public interface AttributeMapper { /** * Returns the GraphQLType for the given Class. If this mapper doesn't know how to handle this particular class, * it MUST return an empty Optional. * * @param javaType * @return */ Optional getBasicAttributeType(Class javaType); } ================================================ FILE: src/main/java/org/crygier/graphql/ExtendedJpaDataFetcher.java ================================================ package org.crygier.graphql; import graphql.language.Argument; import graphql.language.Field; import graphql.language.IntValue; import graphql.language.ObjectValue; import graphql.schema.DataFetchingEnvironment; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.SingularAttribute; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class ExtendedJpaDataFetcher extends JpaDataFetcher { public ExtendedJpaDataFetcher(EntityManager entityManager, EntityType entityType) { super(entityManager, entityType); } @Override public Object get(DataFetchingEnvironment environment) { Field field = environment.getFields().iterator().next(); Map result = new LinkedHashMap<>(); PageInformation pageInformation = extractPageInformation(environment, field); // See which fields we're requesting Optional totalPagesSelection = getSelectionField(field, "totalPages"); Optional totalElementsSelection = getSelectionField(field, "totalElements"); Optional contentSelection = getSelectionField(field, "content"); if (contentSelection.isPresent()) result.put("content", getQuery(environment, contentSelection.get()).setMaxResults(pageInformation.size).setFirstResult((pageInformation.page - 1) * pageInformation.size).getResultList()); if (totalElementsSelection.isPresent() || totalPagesSelection.isPresent()) { final Long totalElements = contentSelection .map(contentField -> getCountQuery(environment, contentField).getSingleResult()) // if no "content" was selected an empty Field can be used .orElseGet(() -> getCountQuery(environment, new Field()).getSingleResult()); result.put("totalElements", totalElements); result.put("totalPages", ((Double) Math.ceil(totalElements / (double) pageInformation.size)).longValue()); } return result; } private TypedQuery getCountQuery(DataFetchingEnvironment environment, Field field) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Long.class); Root root = query.from(entityType); SingularAttribute idAttribute = entityType.getId(Object.class); query.select(cb.count(root.get(idAttribute.getName()))); List predicates = field.getArguments().stream().map(it -> cb.equal(root.get(it.getName()), convertValue(environment, it, it.getValue()))).collect(Collectors.toList()); query.where(predicates.toArray(new Predicate[predicates.size()])); return entityManager.createQuery(query); } private Optional getSelectionField(Field field, String fieldName) { return field.getSelectionSet().getSelections().stream().filter(it -> it instanceof Field).map(it -> (Field) it).filter(it -> fieldName.equals(it.getName())).findFirst(); } private PageInformation extractPageInformation(DataFetchingEnvironment environment, Field field) { Optional paginationRequest = field.getArguments().stream().filter(it -> GraphQLSchemaBuilder.PAGINATION_REQUEST_PARAM_NAME.equals(it.getName())).findFirst(); if (paginationRequest.isPresent()) { field.getArguments().remove(paginationRequest.get()); ObjectValue paginationValues = (ObjectValue) paginationRequest.get().getValue(); IntValue page = (IntValue) paginationValues.getObjectFields().stream().filter(it -> "page".equals(it.getName())).findFirst().get().getValue(); IntValue size = (IntValue) paginationValues.getObjectFields().stream().filter(it -> "size".equals(it.getName())).findFirst().get().getValue(); return new PageInformation(page.getValue().intValue(), size.getValue().intValue()); } return new PageInformation(1, Integer.MAX_VALUE); } private static final class PageInformation { public Integer page; public Integer size; public PageInformation(Integer page, Integer size) { this.page = page; this.size = size; } } } ================================================ FILE: src/main/java/org/crygier/graphql/GraphQLExecutor.java ================================================ package org.crygier.graphql; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.schema.GraphQLSchema; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.persistence.EntityManager; import javax.transaction.Transactional; import java.util.Collection; import java.util.Map; /** * A GraphQL executor capable of constructing a {@link GraphQLSchema} from a JPA {@link EntityManager}. The executor * uses the constructed schema to execute queries directly from the JPA data source. *

* If the executor is given a mutator function, it is feasible to manipulate the {@link GraphQLSchema}, introducing * the option to add mutations, subscriptions etc. */ public class GraphQLExecutor { @Resource private EntityManager entityManager; private GraphQL graphQL; private GraphQLSchema graphQLSchema; private GraphQLSchema.Builder builder; protected GraphQLExecutor() { createGraphQL(null); } /** * Creates a read-only GraphQLExecutor using the entities discovered from the given {@link EntityManager}. * * @param entityManager The entity manager from which the JPA classes annotated with * {@link javax.persistence.Entity} is extracted as {@link GraphQLSchema} objects. */ public GraphQLExecutor(EntityManager entityManager) { this.entityManager = entityManager; createGraphQL(null); } /** * Creates a read-only GraphQLExecutor using the entities discovered from the given {@link EntityManager}. * * @param entityManager The entity manager from which the JPA classes annotated with * {@link javax.persistence.Entity} is extracted as {@link GraphQLSchema} objects. * @param attributeMappers Custom {@link AttributeMapper} list, if you need any non-standard mappings. */ public GraphQLExecutor(EntityManager entityManager, Collection attributeMappers) { this.entityManager = entityManager; createGraphQL(attributeMappers); } @PostConstruct protected synchronized void createGraphQL() { createGraphQL(null); } protected synchronized void createGraphQL(Collection attributeMappers) { if (entityManager != null) { if (builder == null && attributeMappers == null) { this.builder = new GraphQLSchemaBuilder(entityManager); } else if (builder == null) { this.builder = new GraphQLSchemaBuilder(entityManager, attributeMappers); } this.graphQLSchema = builder.build(); this.graphQL = GraphQL.newGraphQL(graphQLSchema).build(); } } /** * @return The {@link GraphQLSchema} used by this executor. */ public GraphQLSchema getGraphQLSchema() { return graphQLSchema; } @Transactional public ExecutionResult execute(String query) { return graphQL.execute(query); } @Transactional public ExecutionResult execute(String query, Map arguments) { if (arguments == null) return graphQL.execute(query); return graphQL.execute(ExecutionInput.newExecutionInput().query(query).variables(arguments).build()); } /** * Gets the builder that was used to create the Schema that this executor is basing its query executions on. The * builder can be used to update the executor with the {@link #updateSchema(GraphQLSchema.Builder)} method. * @return An instance of a builder. */ public GraphQLSchema.Builder getBuilder() { return builder; } /** * Returns the schema that this executor bases its queries on. * @return An instance of a {@link GraphQLSchema}. */ public GraphQLSchema getSchema() { return graphQLSchema; } /** * Uses the given builder to re-create and replace the {@link GraphQLSchema} * that this executor uses to execute its queries. * * @param builder The builder to recreate the current {@link GraphQLSchema} and {@link GraphQL} instances. * @return The same executor but with a new {@link GraphQL} schema. */ public GraphQLExecutor updateSchema(GraphQLSchema.Builder builder) { this.builder = builder; createGraphQL(null); return this; } /** * Uses the given builder to re-create and replace the {@link GraphQLSchema} * that this executor uses to execute its queries. * * @param builder The builder to recreate the current {@link GraphQLSchema} and {@link GraphQL} instances. * @param attributeMappers Custom {@link AttributeMapper} list, if you need any non-standard mappings. * @return The same executor but with a new {@link GraphQL} schema. */ public GraphQLExecutor updateSchema(GraphQLSchema.Builder builder, Collection attributeMappers) { this.builder = builder; createGraphQL(attributeMappers); return this; } } ================================================ FILE: src/main/java/org/crygier/graphql/GraphQLSchemaBuilder.java ================================================ package org.crygier.graphql; import graphql.Scalars; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLEnumType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeReference; import org.crygier.graphql.annotation.GraphQLIgnore; import org.crygier.graphql.annotation.SchemaDocumentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.persistence.EntityManager; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EmbeddableType; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; /** * A wrapper for the {@link GraphQLSchema.Builder}. In addition to exposing the traditional builder functionality, * this class constructs an initial {@link GraphQLSchema} by scanning the given {@link EntityManager} for relevant * JPA entities. This happens at construction time. * * Note: This class should not be accessed outside this library. */ public class GraphQLSchemaBuilder extends GraphQLSchema.Builder { public static final String PAGINATION_REQUEST_PARAM_NAME = "paginationRequest"; private static final Logger log = LoggerFactory.getLogger(GraphQLSchemaBuilder.class); private final EntityManager entityManager; private final Map classCache = new HashMap<>(); private final Map, GraphQLObjectType> embeddableCache = new HashMap<>(); private final Map entityCache = new HashMap<>(); private final List attributeMappers = new ArrayList<>(); /** * Initialises the builder with the given {@link EntityManager} from which we immediately start to scan for * entities to include in the GraphQL schema. * @param entityManager The manager containing the data models to include in the final GraphQL schema. */ public GraphQLSchemaBuilder(EntityManager entityManager) { this.entityManager = entityManager; populateStandardAttributeMappers(); super.query(getQueryType()); } public GraphQLSchemaBuilder(EntityManager entityManager, Collection attributeMappers) { this.entityManager = entityManager; this.attributeMappers.addAll(attributeMappers); populateStandardAttributeMappers(); super.query(getQueryType()); } private void populateStandardAttributeMappers() { attributeMappers.add(createStandardAttributeMapper(UUID.class, JavaScalars.GraphQLUUID)); attributeMappers.add(createStandardAttributeMapper(Date.class, JavaScalars.GraphQLDate)); attributeMappers.add(createStandardAttributeMapper(LocalDateTime.class, JavaScalars.GraphQLLocalDateTime)); attributeMappers.add(createStandardAttributeMapper(Instant.class, JavaScalars.GraphQLInstant)); attributeMappers.add(createStandardAttributeMapper(LocalDate.class, JavaScalars.GraphQLLocalDate)); } private AttributeMapper createStandardAttributeMapper(final Class assignableClass, final GraphQLType type) { return (javaType) -> { if (assignableClass.isAssignableFrom(javaType)) return Optional.of(type); return Optional.empty(); }; } /** * @deprecated Use {@link #build()} instead. * @return A freshly built {@link GraphQLSchema} */ @Deprecated() public GraphQLSchema getGraphQLSchema() { return super.build(); } GraphQLObjectType getQueryType() { GraphQLObjectType.Builder queryType = GraphQLObjectType.newObject().name("QueryType_JPA").description("All encompassing schema for this JPA environment"); queryType.fields(entityManager.getMetamodel().getEntities().stream().filter(this::isNotIgnored).map(this::getQueryFieldDefinition).collect(Collectors.toList())); queryType.fields(entityManager.getMetamodel().getEntities().stream().filter(this::isNotIgnored).map(this::getQueryFieldPageableDefinition).collect(Collectors.toList())); queryType.fields(entityManager.getMetamodel().getEmbeddables().stream().filter(this::isNotIgnored).map(this::getQueryEmbeddedFieldDefinition).collect(Collectors.toList())); return queryType.build(); } GraphQLFieldDefinition getQueryFieldDefinition(EntityType entityType) { return GraphQLFieldDefinition.newFieldDefinition() .name(entityType.getName()) .description(getSchemaDocumentation(entityType.getJavaType())) .type(new GraphQLList(getObjectType(entityType))) .dataFetcher(new JpaDataFetcher(entityManager, entityType)) .argument(entityType.getAttributes().stream().filter(this::isValidInput).filter(this::isNotIgnored).flatMap(this::getArgument).collect(Collectors.toList())) .build(); } GraphQLFieldDefinition getQueryEmbeddedFieldDefinition(EmbeddableType embeddableType) { String embeddedName = embeddableType.getJavaType().getSimpleName(); return GraphQLFieldDefinition.newFieldDefinition() .name(embeddedName) .description(getSchemaDocumentation(embeddableType.getJavaType())) .type(new GraphQLList(getObjectType(embeddableType))) .argument(embeddableType.getAttributes().stream().filter(this::isValidInput).filter(this::isNotIgnored).flatMap(this::getArgument).collect(Collectors.toList())) .build(); } private GraphQLFieldDefinition getQueryFieldPageableDefinition(EntityType entityType) { GraphQLObjectType pageType = GraphQLObjectType.newObject() .name(entityType.getName() + "Connection") .description("'Connection' response wrapper object for " + entityType.getName() + ". When pagination or aggregation is requested, this object will be returned with metadata about the query.") .field(GraphQLFieldDefinition.newFieldDefinition().name("totalPages").description("Total number of pages calculated on the database for this pageSize.").type(Scalars.GraphQLLong).build()) .field(GraphQLFieldDefinition.newFieldDefinition().name("totalElements").description("Total number of results on the database for this query.").type(Scalars.GraphQLLong).build()) .field(GraphQLFieldDefinition.newFieldDefinition().name("content").description("The actual object results").type(new GraphQLList(getObjectType(entityType))).build()) .build(); return GraphQLFieldDefinition.newFieldDefinition() .name(entityType.getName() + "Connection") .description("'Connection' request wrapper object for " + entityType.getName() + ". Use this object in a query to request things like pagination or aggregation in an argument. Use the 'content' field to request actual fields ") .type(pageType) .dataFetcher(new ExtendedJpaDataFetcher(entityManager, entityType)) .argument(paginationArgument) .build(); } private Stream getArgument(Attribute attribute) { return getAttributeType(attribute) .filter(type -> type instanceof GraphQLInputType) .filter(type -> attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.EMBEDDED || (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED && type instanceof GraphQLScalarType)) .map(type -> { String name = attribute.getName(); return GraphQLArgument.newArgument() .name(name) .type((GraphQLInputType) type) .build(); }); } GraphQLObjectType getObjectType(EntityType entityType) { if (entityCache.containsKey(entityType)) return entityCache.get(entityType); GraphQLObjectType answer = GraphQLObjectType.newObject() .name(entityType.getName()) .description(getSchemaDocumentation(entityType.getJavaType())) .fields(entityType.getAttributes().stream().filter(this::isNotIgnored).flatMap(this::getObjectField).collect(Collectors.toList())) .build(); entityCache.put(entityType, answer); return answer; } GraphQLObjectType getObjectType(EmbeddableType embeddableType) { if (embeddableCache.containsKey(embeddableType)) return embeddableCache.get(embeddableType); String embeddableName= embeddableType.getJavaType().getSimpleName(); GraphQLObjectType answer = GraphQLObjectType.newObject() .name(embeddableName) .description(getSchemaDocumentation(embeddableType.getJavaType())) .fields(embeddableType.getAttributes().stream().filter(this::isNotIgnored).flatMap(this::getObjectField).collect(Collectors.toList())) .build(); embeddableCache.put(embeddableType, answer); return answer; } private Stream getObjectField(Attribute attribute) { return getAttributeType(attribute) .filter(type -> type instanceof GraphQLOutputType) .map(type -> { List arguments = new ArrayList<>(); arguments.add(GraphQLArgument.newArgument().name("orderBy").type(orderByDirectionEnum).build()); // Always add the orderBy argument // Get the fields that can be queried on (i.e. Simple Types, no Sub-Objects) if (attribute instanceof SingularAttribute && attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC) { ManagedType foreignType = (ManagedType) ((SingularAttribute) attribute).getType(); Stream attributes = findBasicAttributes(foreignType.getAttributes()); attributes.forEach(it -> { arguments.add(GraphQLArgument.newArgument() .name(it.getName()) .type((GraphQLInputType) getAttributeType(it).findFirst().get()) .build()); }); } String name = attribute.getName(); return GraphQLFieldDefinition.newFieldDefinition() .name(name) .description(getSchemaDocumentation(attribute.getJavaMember())) .type((GraphQLOutputType) type) .argument(arguments) .build(); }); } private Stream findBasicAttributes(Collection attributes) { return attributes.stream().filter(this::isNotIgnored).filter(it -> it.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC); } private GraphQLType getBasicAttributeType(Class javaType) { // First check our 'standard' and 'customized' Attribute Mappers. Use them if possible Optional customMapper = attributeMappers.stream() .filter(it -> it.getBasicAttributeType(javaType).isPresent()) .findFirst(); if (customMapper.isPresent()) return customMapper.get().getBasicAttributeType(javaType).get(); else if (String.class.isAssignableFrom(javaType)) return Scalars.GraphQLString; else if (Integer.class.isAssignableFrom(javaType) || int.class.isAssignableFrom(javaType)) return Scalars.GraphQLInt; else if (Short.class.isAssignableFrom(javaType) || short.class.isAssignableFrom(javaType)) return Scalars.GraphQLShort; else if (Float.class.isAssignableFrom(javaType) || float.class.isAssignableFrom(javaType) || Double.class.isAssignableFrom(javaType) || double.class.isAssignableFrom(javaType)) return Scalars.GraphQLFloat; else if (Long.class.isAssignableFrom(javaType) || long.class.isAssignableFrom(javaType)) return Scalars.GraphQLLong; else if (Boolean.class.isAssignableFrom(javaType) || boolean.class.isAssignableFrom(javaType)) return Scalars.GraphQLBoolean; else if (javaType.isEnum()) { return getTypeFromJavaType(javaType); } else if (BigDecimal.class.isAssignableFrom(javaType)) { return Scalars.GraphQLBigDecimal; } throw new UnsupportedOperationException( "Class could not be mapped to GraphQL: '" + javaType.getClass().getTypeName() + "'"); } private Stream getAttributeType(Attribute attribute) { if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC) { try { return Stream.of(getBasicAttributeType(attribute.getJavaType())); } catch (UnsupportedOperationException e) { //fall through to the exception below //which is more useful because it also contains the declaring member } } else if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY) { EntityType foreignType = (EntityType) ((PluralAttribute) attribute).getElementType(); return Stream.of(new GraphQLList(new GraphQLTypeReference(foreignType.getName()))); } else if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE) { EntityType foreignType = (EntityType) ((SingularAttribute) attribute).getType(); return Stream.of(new GraphQLTypeReference(foreignType.getName())); } else if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ELEMENT_COLLECTION) { Type foreignType = ((PluralAttribute) attribute).getElementType(); return Stream.of(new GraphQLList(getTypeFromJavaType(foreignType.getJavaType()))); } else if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED) { EmbeddableType embeddableType = (EmbeddableType) ((SingularAttribute) attribute).getType(); return Stream.of(new GraphQLTypeReference(embeddableType.getJavaType().getSimpleName())); } final String declaringType = attribute.getDeclaringType().getJavaType().getName(); // fully qualified name of the entity class final String declaringMember = attribute.getJavaMember().getName(); // field name in the entity class throw new UnsupportedOperationException( "Attribute could not be mapped to GraphQL: field '" + declaringMember + "' of entity class '" + declaringType + "'"); } private boolean isValidInput(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ELEMENT_COLLECTION || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED; } private String getSchemaDocumentation(Member member) { if (member instanceof AnnotatedElement) { return getSchemaDocumentation((AnnotatedElement) member); } return null; } private String getSchemaDocumentation(AnnotatedElement annotatedElement) { if (annotatedElement != null) { SchemaDocumentation schemaDocumentation = annotatedElement.getAnnotation(SchemaDocumentation.class); return schemaDocumentation != null ? schemaDocumentation.value() : null; } return null; } private boolean isNotIgnored(Attribute attribute) { return isNotIgnored(attribute.getJavaMember()) && isNotIgnored(attribute.getJavaType()); } private boolean isNotIgnored(EmbeddableType embeddableType) { return isNotIgnored(embeddableType.getJavaType()); } private boolean isNotIgnored(EntityType entityType) { return isNotIgnored(entityType.getJavaType()); } private boolean isNotIgnored(Member member) { return member instanceof AnnotatedElement && isNotIgnored((AnnotatedElement) member); } private boolean isNotIgnored(AnnotatedElement annotatedElement) { if (annotatedElement != null) { GraphQLIgnore schemaDocumentation = annotatedElement.getAnnotation(GraphQLIgnore.class); return schemaDocumentation == null; } return false; } private GraphQLType getTypeFromJavaType(Class clazz) { if (clazz.isEnum()) { if (classCache.containsKey(clazz)) return classCache.get(clazz); GraphQLEnumType.Builder enumBuilder = GraphQLEnumType.newEnum().name(clazz.getSimpleName()); int ordinal = 0; for (Enum enumValue : ((Class) clazz).getEnumConstants()) enumBuilder.value(enumValue.name(), ordinal++); GraphQLType answer = enumBuilder.build(); setIdentityCoercing(answer); classCache.put(clazz, answer); return answer; } return getBasicAttributeType(clazz); } /** * A bit of a hack, since JPA will deserialize our Enum's for us...we don't want GraphQL doing it. * * @param type */ private void setIdentityCoercing(GraphQLType type) { try { Field coercing = type.getClass().getDeclaredField("coercing"); coercing.setAccessible(true); coercing.set(type, new IdentityCoercing()); } catch (Exception e) { log.error("Unable to set coercing for " + type, e); } } private static final GraphQLArgument paginationArgument = GraphQLArgument.newArgument() .name(PAGINATION_REQUEST_PARAM_NAME) .type(GraphQLInputObjectType.newInputObject() .name("PaginationObject") .description("Query object for Pagination Requests, specifying the requested page, and that page's size.\n\nNOTE: 'page' parameter is 1-indexed, NOT 0-indexed.\n\nExample: paginationRequest { page: 1, size: 20 }") .field(GraphQLInputObjectField.newInputObjectField().name("page").description("Which page should be returned, starting with 1 (1-indexed)").type(Scalars.GraphQLInt).build()) .field(GraphQLInputObjectField.newInputObjectField().name("size").description("How many results should this page contain").type(Scalars.GraphQLInt).build()) .build() ).build(); private static final GraphQLEnumType orderByDirectionEnum = GraphQLEnumType.newEnum() .name("OrderByDirection") .description("Describes the direction (Ascending / Descending) to sort a field.") .value("ASC", 0, "Ascending") .value("DESC", 1, "Descending") .build(); } ================================================ FILE: src/main/java/org/crygier/graphql/IdentityCoercing.java ================================================ package org.crygier.graphql; import graphql.schema.Coercing; public class IdentityCoercing implements Coercing{ @Override public Object serialize(Object input) { return input; } @Override public Object parseValue(Object input) { return input; } @Override public Object parseLiteral(Object input) { return input; } } ================================================ FILE: src/main/java/org/crygier/graphql/JavaScalars.java ================================================ package org.crygier.graphql; import graphql.language.IntValue; import graphql.language.StringValue; import graphql.schema.Coercing; import graphql.schema.CoercingSerializeException; import graphql.schema.GraphQLScalarType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.text.DateFormat; import java.text.ParseException; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; import java.util.Date; import java.util.TimeZone; import java.util.UUID; public class JavaScalars { static final Logger log = LoggerFactory.getLogger(JavaScalars.class); public static GraphQLScalarType GraphQLLocalDateTime = new GraphQLScalarType("LocalDateTime", "Date type", new Coercing() { @Override public Object serialize(Object input) { if (input instanceof String) { return parseStringToLocalDateTime((String) input); } else if (input instanceof LocalDateTime) { return input; } else if (input instanceof Long) { return parseLongToLocalDateTime((Long) input); } else if (input instanceof Integer) { return parseLongToLocalDateTime((Integer) input); } return null; } @Override public Object parseValue(Object input) { return serialize(input); } @Override public Object parseLiteral(Object input) { if (input instanceof StringValue) { return parseStringToLocalDateTime(((StringValue) input).getValue()); } else if (input instanceof IntValue) { BigInteger value = ((IntValue) input).getValue(); return parseLongToLocalDateTime(value.longValue()); } return null; } private LocalDateTime parseLongToLocalDateTime(long input) { return LocalDateTime.ofInstant(Instant.ofEpochSecond(input), TimeZone.getDefault().toZoneId()); } private LocalDateTime parseStringToLocalDateTime(String input) { try { return LocalDateTime.parse(input); } catch (DateTimeParseException e) { log.warn("Failed to parse Date from input: " + input, e); return null; } } }); public static GraphQLScalarType GraphQLInstant = new GraphQLScalarType("Instant", "Date type", new Coercing() { @Override public Long serialize(Object input) { if (input instanceof Instant) { return ((Instant) input).getEpochSecond(); } throw new CoercingSerializeException( "Expected type 'Instant' but was '" + input.getClass().getSimpleName() + "'."); } @Override public Instant parseValue(Object input) { if (input instanceof Long) { return Instant.ofEpochSecond((Long) input); } else if (input instanceof Integer) { return Instant.ofEpochSecond((Integer) input); } throw new CoercingSerializeException( "Expected type 'Long' or 'Integer' but was '" + input.getClass().getSimpleName() + "'."); } @Override public Instant parseLiteral(Object input) { if (input instanceof IntValue) { return Instant.ofEpochSecond(((IntValue) input).getValue().longValue()); } return null; } }); public static GraphQLScalarType GraphQLLocalDate = new GraphQLScalarType("LocalDate", "Date type", new Coercing() { @Override public Object serialize(Object input) { if (input instanceof String) { return parseStringToLocalDate((String) input); } else if (input instanceof LocalDate) { return input; } else if (input instanceof Long) { return parseLongToLocalDate((Long) input); } else if (input instanceof Integer) { return parseLongToLocalDate((Integer) input); } return null; } @Override public Object parseValue(Object input) { return serialize(input); } @Override public Object parseLiteral(Object input) { if (input instanceof StringValue) { return parseStringToLocalDate(((StringValue) input).getValue()); } else if (input instanceof IntValue) { BigInteger value = ((IntValue) input).getValue(); return parseLongToLocalDate(value.longValue()); } return null; } private LocalDate parseLongToLocalDate(long input) { return LocalDateTime.ofInstant(Instant.ofEpochSecond(input), TimeZone.getDefault().toZoneId()).toLocalDate(); } private LocalDate parseStringToLocalDate(String input) { try { return LocalDate.parse(input); } catch (DateTimeParseException e) { log.warn("Failed to parse Date from input: " + input, e); return null; } } }); public static GraphQLScalarType GraphQLDate = new GraphQLScalarType("Date", "Date type", new Coercing() { @Override public Object serialize(Object input) { if (input instanceof String) { return parseStringToDate((String) input); } else if (input instanceof Date) { return input; } else if (input instanceof Long) { return new Date(((Long) input).longValue()); } else if (input instanceof Integer) { return new Date(((Integer) input).longValue()); } return null; } @Override public Object parseValue(Object input) { return serialize(input); } @Override public Object parseLiteral(Object input) { if (input instanceof StringValue) { return parseStringToDate(((StringValue) input).getValue()); } else if (input instanceof IntValue) { BigInteger value = ((IntValue) input).getValue(); return new Date(value.longValue()); } return null; } private Date parseStringToDate(String input) { try { return DateFormat.getInstance().parse(input); } catch (ParseException e) { log.warn("Failed to parse Date from input: " + input, e); return null; } } }); public static GraphQLScalarType GraphQLUUID = new GraphQLScalarType("UUID", "UUID type", new Coercing() { @Override public Object serialize(Object input) { if (input instanceof UUID) { return input; } return null; } @Override public Object parseValue(Object input) { if (input instanceof String) { return parseStringToUUID((String) input); } return null; } @Override public Object parseLiteral(Object input) { if (input instanceof StringValue) { return parseStringToUUID(((StringValue) input).getValue()); } return null; } private UUID parseStringToUUID(String input) { try { return UUID.fromString(input); } catch (IllegalArgumentException e) { log.warn("Failed to parse UUID from input: " + input, e); return null; } } }); } ================================================ FILE: src/main/java/org/crygier/graphql/JpaDataFetcher.java ================================================ package org.crygier.graphql; import graphql.language.*; import graphql.schema.*; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.persistence.criteria.*; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.SingularAttribute; import java.util.*; import java.util.stream.Collectors; public class JpaDataFetcher implements DataFetcher { protected EntityManager entityManager; protected EntityType entityType; public JpaDataFetcher(EntityManager entityManager, EntityType entityType) { this.entityManager = entityManager; this.entityType = entityType; } @Override public Object get(DataFetchingEnvironment environment) { return getQuery(environment, environment.getFields().iterator().next()).getResultList(); } protected TypedQuery getQuery(DataFetchingEnvironment environment, Field field) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery((Class) entityType.getJavaType()); Root root = query.from(entityType); List arguments = new ArrayList<>(); // Loop through all of the fields being requested field.getSelectionSet().getSelections().forEach(selection -> { if (selection instanceof Field) { Field selectedField = (Field) selection; // "__typename" is part of the graphql introspection spec and has to be ignored by jpa if(!"__typename".equals(selectedField.getName())) { Path fieldPath = root.get(selectedField.getName()); // Process the orderBy clause Optional orderByArgument = selectedField.getArguments().stream().filter(it -> "orderBy".equals(it.getName())).findFirst(); if (orderByArgument.isPresent()) { if ("DESC".equals(((EnumValue) orderByArgument.get().getValue()).getName())) query.orderBy(cb.desc(fieldPath)); else query.orderBy(cb.asc(fieldPath)); } // Process arguments clauses arguments.addAll(selectedField.getArguments().stream() .filter(it -> !"orderBy".equals(it.getName())) .map(it -> new Argument(selectedField.getName() + "." + it.getName(), it.getValue())) .collect(Collectors.toList())); // Check if it's an object and the foreign side is One. Then we can eagerly fetch causing an inner join instead of 2 queries if (fieldPath.getModel() instanceof SingularAttribute) { SingularAttribute attribute = (SingularAttribute) fieldPath.getModel(); if (!attribute.isOptional() && (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE)) root.fetch(selectedField.getName()); } } } }); arguments.addAll(field.getArguments()); List predicates = arguments.stream().map(it -> getPredicate(cb, root, environment, it)).collect(Collectors.toList()); query.where(predicates.toArray(new Predicate[predicates.size()])); return entityManager.createQuery(query.distinct(true)); } private Predicate getPredicate(CriteriaBuilder cb, Root root, DataFetchingEnvironment environment, Argument argument) { Path path = null; if (!argument.getName().contains(".")) { Attribute argumentEntityAttribute = getAttribute(environment, argument); // If the argument is a list, let's assume we need to join and do an 'in' clause if (argumentEntityAttribute instanceof PluralAttribute) { Join join = root.join(argument.getName()); return join.in(convertValue(environment, argument, argument.getValue())); } path = root.get(argument.getName()); return cb.equal(path, convertValue(environment, argument, argument.getValue())); } else { List parts = Arrays.asList(argument.getName().split("\\.")); for (String part : parts) { if (path == null) { path = root.get(part); } else { path = path.get(part); } } return cb.equal(path, convertValue(environment, argument, argument.getValue())); } } protected Object convertValue(DataFetchingEnvironment environment, Argument argument, Value value) { if (value instanceof StringValue) { Object convertedValue = environment.getArgument(argument.getName()); if (convertedValue != null) { // Return real parameter for instance UUID even if the Value is a StringValue return convertedValue; } else { // Return provided StringValue return ((StringValue) value).getValue(); } } else if (value instanceof VariableReference) return environment.getArguments().get(((VariableReference) value).getName()); else if (value instanceof ArrayValue) return ((ArrayValue) value).getValues().stream().map((it) -> convertValue(environment, argument, it)).collect(Collectors.toList()); else if (value instanceof EnumValue) { Class enumType = getJavaType(environment, argument); return Enum.valueOf(enumType, ((EnumValue) value).getName()); } else if (value instanceof IntValue) { return ((IntValue) value).getValue(); } else if (value instanceof BooleanValue) { return ((BooleanValue) value).isValue(); } else if (value instanceof FloatValue) { return ((FloatValue) value).getValue(); } return value.toString(); } private Class getJavaType(DataFetchingEnvironment environment, Argument argument) { Attribute argumentEntityAttribute = getAttribute(environment, argument); if (argumentEntityAttribute instanceof PluralAttribute) return ((PluralAttribute) argumentEntityAttribute).getElementType().getJavaType(); return argumentEntityAttribute.getJavaType(); } private Attribute getAttribute(DataFetchingEnvironment environment, Argument argument) { GraphQLObjectType objectType = getObjectType(environment, argument); EntityType entityType = getEntityType(objectType); return entityType.getAttribute(argument.getName()); } private EntityType getEntityType(GraphQLObjectType objectType) { return entityManager.getMetamodel().getEntities().stream().filter(it -> it.getName().equals(objectType.getName())).findFirst().get(); } private GraphQLObjectType getObjectType(DataFetchingEnvironment environment, Argument argument) { GraphQLType outputType = environment.getFieldType(); if (outputType instanceof GraphQLList) outputType = ((GraphQLList) outputType).getWrappedType(); if (outputType instanceof GraphQLObjectType) return (GraphQLObjectType) outputType; return null; } } ================================================ FILE: src/main/java/org/crygier/graphql/annotation/GraphQLIgnore.java ================================================ package org.crygier.graphql.annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target( { TYPE, FIELD }) @Retention(RUNTIME) public @interface GraphQLIgnore { } ================================================ FILE: src/main/java/org/crygier/graphql/annotation/SchemaDocumentation.java ================================================ package org.crygier.graphql.annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target( { TYPE, FIELD }) @Retention(RUNTIME) public @interface SchemaDocumentation { String value(); } ================================================ FILE: src/test/groovy/org/crygier/graphql/EmbeddedQueryExecutorTest.groovy ================================================ package org.crygier.graphql import javax.persistence.EntityManager import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.context.annotation.Configuration import org.springframework.test.context.ContextConfiguration import spock.lang.Ignore import spock.lang.Specification @Configuration @ContextConfiguration(loader = SpringBootContextLoader, classes = TestApplication) class EmbeddedQueryExecutorTest extends Specification { @Autowired private GraphQLExecutor executor; def 'Query Embedded Values'() { given: def query = ''' { Spaceship (id: "1000"){ name, created { user {id}}, modified {date} } } ''' def expected = [ Spaceship: [[name: "X-Wing", created:[user:[id:"1000"]], modified:null]] ] when: def result = executor.execute(query).data then: result == expected } } ================================================ FILE: src/test/groovy/org/crygier/graphql/EmbeddedSchemaBuildTest.groovy ================================================ package org.crygier.graphql import graphql.schema.GraphQLObjectType import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.context.annotation.Configuration import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import javax.persistence.EntityManager import javax.persistence.metamodel.EntityType import java.util.stream.Collectors @Configuration @ContextConfiguration(loader = SpringBootContextLoader, classes = TestApplication) class EmbeddedSchemaBuildTest extends Specification { @Autowired private EntityManager entityManager; private GraphQLSchemaBuilder builder; void setup() { builder = new GraphQLSchemaBuilder(entityManager); } def 'Correctly read embedded keys'() { when: def embeddingEntity = entityManager.getMetamodel().getEntities().stream().filter { e -> e.name == "EmbeddingTest"}.findFirst().get() def graphQlObject = builder.getObjectType(embeddingEntity) then: graphQlObject.fieldDefinitions.size() == 1 } def 'Correctly extract embedded basic query fields'() { when: def embeddingEntity = entityManager.getMetamodel().getEntities().stream().filter { e -> e.name == "EmbeddingTest"}.findFirst().get() def graphQlFieldDefinition = builder.getQueryFieldDefinition(embeddingEntity) then: graphQlFieldDefinition.arguments.size() == 0 } def 'Correctly extract a whole moddel with embeddings'() { when: def q = builder.getQueryType() then: true } } ================================================ FILE: src/test/groovy/org/crygier/graphql/GraphQlController.groovy ================================================ package org.crygier.graphql import com.fasterxml.jackson.databind.ObjectMapper import graphql.ExecutionResult import groovy.transform.CompileStatic import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.bind.annotation.RestController @RestController @CompileStatic class GraphQlController { @Autowired private GraphQLExecutor graphQLExecutor; @Autowired private ObjectMapper objectMapper; @RequestMapping(path = '/graphql', method = RequestMethod.POST) ExecutionResult graphQl(@RequestBody final GraphQLInputQuery query) { Map variables = query.getVariables() ? objectMapper.readValue(query.getVariables(), Map) : null; return graphQLExecutor.execute(query.getQuery(), variables); } public static final class GraphQLInputQuery { String query; String variables; } } ================================================ FILE: src/test/groovy/org/crygier/graphql/JavaScalarsTest.groovy ================================================ package org.crygier.graphql import graphql.schema.Coercing import spock.lang.Specification import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.Month import java.time.ZoneId class JavaScalarsTest extends Specification { def 'Long to LocalDateTime'() { given: Coercing coercing = JavaScalars.GraphQLLocalDateTime.getCoercing() LocalDateTime localDateTime = LocalDateTime.of(2017, 02, 02, 12, 30, 15) long input = localDateTime.toEpochSecond(ZoneId.systemDefault().getRules().getOffset(localDateTime)) when: def result = coercing.serialize(input) then: result instanceof LocalDateTime result.dayOfMonth == 2 result.month == Month.FEBRUARY result.year == 2017 result.hour == 12 result.minute == 30 result.second == 15 } def 'String to LocalDateTime'() { given: Coercing coercing = JavaScalars.GraphQLLocalDateTime.getCoercing() final String input = "2017-02-02T12:30:15" when: def result = coercing.serialize(input) then: result instanceof LocalDateTime result.dayOfMonth == 2 result.month == Month.FEBRUARY result.year == 2017 result.hour == 12 result.minute == 30 result.second == 15 } def 'Instant to Long'() { given: Coercing coercing = JavaScalars.GraphQLInstant.getCoercing() final instant = Instant.now() when: def result = coercing.serialize(instant) then: result instanceof Long result == instant.epochSecond } def 'Long to LocalDate'() { given: Coercing coercing = JavaScalars.GraphQLLocalDate.getCoercing() LocalDateTime localDateTime = LocalDateTime.of(2017, 02, 02, 0, 0, 0) long input = localDateTime.toEpochSecond(ZoneId.systemDefault().getRules().getOffset(localDateTime)) when: def result = coercing.serialize(input) then: result instanceof LocalDate result.dayOfMonth == 2 result.month == Month.FEBRUARY result.year == 2017 } def 'String to LocalDate'() { given: Coercing coercing = JavaScalars.GraphQLLocalDate.getCoercing() final String input = "2017-02-02" when: def result = coercing.serialize(input) then: result instanceof LocalDate result.dayOfMonth == 2 result.month == Month.FEBRUARY result.year == 2017 } } ================================================ FILE: src/test/groovy/org/crygier/graphql/MutableQueryExecutorTest.groovy ================================================ package org.crygier.graphql import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.context.annotation.Configuration import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import static graphql.Scalars.GraphQLString @Configuration @ContextConfiguration(loader = SpringBootContextLoader, classes = TestApplication) class MutableQueryExecutorTest extends Specification { @Autowired private GraphQLExecutor executor; private final GraphQLObjectType droidMutation = GraphQLObjectType.newObject() .name("CreateDroidMutation") .field(GraphQLFieldDefinition.newFieldDefinition() .name("name") .type(GraphQLString)) .build() def 'Can add a schema mutation'() { when: GraphQLSchema.Builder builder = executor.getBuilder().mutation(droidMutation) executor.updateSchema(builder) then: executor.getSchema().mutationType == droidMutation } } ================================================ FILE: src/test/groovy/org/crygier/graphql/MutableSchemaBuildTest.groovy ================================================ package org.crygier.graphql import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.context.annotation.Configuration import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import javax.persistence.EntityManager import static graphql.Scalars.GraphQLString @Configuration @ContextConfiguration(loader = SpringBootContextLoader, classes = TestApplication) class MutableSchemaBuildTest extends Specification { @Autowired private EntityManager entityManager; private GraphQLSchema schema; private final GraphQLObjectType droidMutation = GraphQLObjectType.newObject() .name("CreateDroidMutation") .field(GraphQLFieldDefinition.newFieldDefinition() .name("name") .type(GraphQLString)) .build() void setup() { schema = new GraphQLSchemaBuilder(entityManager).mutation(droidMutation).build() } def 'Can add a schema mutation'() { expect: schema.mutationType == droidMutation } } ================================================ FILE: src/test/groovy/org/crygier/graphql/StarwarsQueryExecutorTest.groovy ================================================ package org.crygier.graphql import org.crygier.graphql.model.starwars.Episode import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.context.annotation.Configuration import org.springframework.test.context.ContextConfiguration import org.springframework.transaction.annotation.Transactional import spock.lang.Specification import javax.persistence.EntityManager @Configuration @ContextConfiguration(loader = SpringBootContextLoader, classes = TestApplication) class StarwarsQueryExecutorTest extends Specification { @Autowired private GraphQLExecutor executor; def 'Gets just the names of all droids'() { given: def query = ''' query HeroNameQuery { Droid { name } } ''' def expected = [ Droid: [ [ name: 'C-3PO' ], [ name: 'R2-D2' ] ] ] when: def result = executor.execute(query).data then: result['Droid'].sort() == expected['Droid'] } def 'Query for droid by name'() { given: def query = ''' { Droid(name: "C-3PO") { name primaryFunction } } ''' def expected = [ Droid: [ [ name: 'C-3PO', primaryFunction: 'Protocol' ] ] ] when: def result = executor.execute(query).data then: result == expected } def 'ManyToOne Join by ID'() { given: def query = ''' { Human(id: "1000") { name homePlanet favoriteDroid { name } } } ''' def expected = [ Human: [ [name:'Luke Skywalker', homePlanet:'Tatooine', favoriteDroid:[name:'C-3PO']] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Nullable ManyToOne Join'() { given: def query = ''' { Human(id: "1004") { name homePlanet favoriteDroid { name } } } ''' def expected = [ Human: [ [name:'Wilhuff Tarkin', homePlanet:null, favoriteDroid:null] ] ] when: def result = executor.execute(query).data then: result == expected } def 'OneToMany Join by ID'() { given: def query = ''' { Human(id: "1000") { name homePlanet friends { name } } } ''' def expected = [ Human: [ [name: 'Luke Skywalker', homePlanet: 'Tatooine', friends: [[name: 'Han Solo'], [name: 'Leia Organa'], [name: 'C-3PO'], [name: 'R2-D2']]] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Query with parameter'() { given: def query = ''' query humanQuery($id: String!) { Human(id: $id) { name homePlanet } } ''' def expected = [ Human: [ [name: 'Darth Vader', homePlanet: 'Tatooine'] ] ] when: def result = executor.execute(query, [id: "1001"]).data then: result == expected } def 'Query with alias'() { given: def query = ''' { luke: Human(id: "1000") { name homePlanet } leia: Human(id: "1003") { name } } ''' def expected = [ luke: [ [name: 'Luke Skywalker', homePlanet: 'Tatooine'], ], leia: [ [name: 'Leia Organa'] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Allows us to use a fragment to avoid duplicating content'() { given: def query = """ query UseFragment { luke: Human(id: "1000") { ...HumanFragment } leia: Human(id: "1003") { ...HumanFragment } } fragment HumanFragment on Human { name homePlanet } """ def expected = [ luke: [ [name: 'Luke Skywalker', homePlanet: 'Tatooine'], ], leia: [ [name: 'Leia Organa', homePlanet: 'Alderaan'] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Deep nesting'() { given: def query = ''' { Droid(id: "2001") { name friends { name appearsIn friends { name } } } } ''' def expected = [ Droid:[ [ name:'R2-D2', friends:[ [ name:'Luke Skywalker', appearsIn:['A_NEW_HOPE', 'EMPIRE_STRIKES_BACK', 'RETURN_OF_THE_JEDI', 'THE_FORCE_AWAKENS'], friends:[['name:Han Solo'], [name:'Leia Organa'], [name:'C-3PO'], [name:'R2-D2']]], [ name:'Han Solo', appearsIn:['A_NEW_HOPE', 'EMPIRE_STRIKES_BACK', 'RETURN_OF_THE_JEDI', 'THE_FORCE_AWAKENS'], friends:[[name:'Luke Skywalker'], [name:'Leia Organa'], [name:'R2-D2']]], [ name:'Leia Organa', appearsIn:['A_NEW_HOPE', 'EMPIRE_STRIKES_BACK', 'RETURN_OF_THE_JEDI', 'THE_FORCE_AWAKENS'], friends:[[name:'Luke Skywalker'], [name:'Han Solo'], [name:'C-3PO'], [name:'R2-D2']]] ] ] ] ] when: def result = executor.execute(query).data then: result.toString() == expected.toString() } def 'Pagination at the root'() { given: def query = ''' { HumanConnection(paginationRequest: { page: 1, size: 2 }) { totalPages totalElements content { name } } } ''' def expected = [ HumanConnection: [ totalPages: 3, totalElements: 5, content: [ [ name: 'Darth Vader' ], [ name: 'Luke Skywalker' ] ] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Pagination without content'() { given: def query = ''' { HumanConnection(paginationRequest: { page: 1, size: 2}) { totalPages totalElements } } ''' def expected = [ HumanConnection: [ totalPages: 3, totalElements: 5 ] ] when: def result = executor.execute(query).data then: result == expected } def 'Ordering Fields'() { given: def query = ''' { Human { name(orderBy: DESC) homePlanet } } ''' def expected = [ Human: [ [ name: 'Wilhuff Tarkin', homePlanet: null], [ name: 'Luke Skywalker', homePlanet: "Tatooine"], [ name: 'Leia Organa', homePlanet: "Alderaan"], [ name: 'Han Solo', homePlanet: null], [ name: 'Darth Vader', homePlanet: "Tatooine"] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Query by Collection of Enums at root level'() { // Semi-proper JPA: select distinct h from Human h join h.appearsIn ai where ai in (:episodes) given: def query = ''' { Human(appearsIn: [THE_FORCE_AWAKENS]) { name appearsIn } } ''' def expected = [ Human: [ [ name: 'Leia Organa', appearsIn: [Episode.A_NEW_HOPE, Episode.EMPIRE_STRIKES_BACK, Episode.RETURN_OF_THE_JEDI, Episode.THE_FORCE_AWAKENS] ], [ name: 'Luke Skywalker', appearsIn: [Episode.A_NEW_HOPE, Episode.EMPIRE_STRIKES_BACK, Episode.RETURN_OF_THE_JEDI, Episode.THE_FORCE_AWAKENS]], [ name: 'Han Solo', appearsIn: [Episode.A_NEW_HOPE, Episode.EMPIRE_STRIKES_BACK, Episode.RETURN_OF_THE_JEDI, Episode.THE_FORCE_AWAKENS] ] ] ] when: def result = executor.execute(query).data then: result == expected; } def 'Query by restricting sub-object'() { given: def query = ''' { Human { name gender(code: "Male") { description } } } ''' def expected = [ Human: [ [ name: 'Darth Vader', gender: [ description: "Male" ] ], [ name: 'Luke Skywalker', gender: [ description: "Male" ]], [ name: 'Han Solo', gender: [ description: "Male" ] ], [ name: 'Wilhuff Tarkin', gender: [ description: "Male" ]] ] ] when: def result = executor.execute(query).data then: result == expected; } def 'Query for searching by IntType (sequence field)'() { given: def query = ''' { CodeList(sequence: 2) { id description active type sequence } } ''' def expected = [ CodeList: [ [ id: 1, description: "Female", active: true, type: "org.crygier.graphql.model.starwars.Gender", sequence: 2] ] ] when: def result = executor.execute(query).data then: result == expected; } def 'Query for searching by BooleanType (active field)'() { given: def query = ''' { CodeList(active: true) { id description active type sequence } } ''' def expected = [ CodeList: [ [ id: 0, description: "Male", active: true, type: "org.crygier.graphql.model.starwars.Gender", sequence: 1], [ id: 1, description: "Female", active: true, type: "org.crygier.graphql.model.starwars.Gender", sequence: 2] ] ] when: def result = executor.execute(query).data then: result == expected; } @Autowired private EntityManager em; @Transactional def 'JPA Sample Tester'() { when: //def query = em.createQuery("select h.id, h.name, h.gender.description from Human h where h.gender.code = 'Male'"); def query = em.createQuery("select h, h.friends from Human h"); //query.setParameter(1, Episode.THE_FORCE_AWAKENS); //query.setParameter("episodes", EnumSet.of(Episode.THE_FORCE_AWAKENS)); def result = query.getResultList(); //println JsonOutput.prettyPrint(JsonOutput.toJson(result)); then: result; } } ================================================ FILE: src/test/groovy/org/crygier/graphql/StarwarsSchemaBuildTest.groovy ================================================ package org.crygier.graphql import graphql.Scalars import graphql.schema.GraphQLSchema import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.context.annotation.Configuration import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import javax.persistence.EntityManager @Configuration @ContextConfiguration(loader = SpringBootContextLoader, classes = TestApplication) class StarwarsSchemaBuildTest extends Specification { @Autowired private EntityManager entityManager; private GraphQLSchemaBuilder builder; void setup() { builder = new GraphQLSchemaBuilder(entityManager); } def 'Correctly derives the schema from Given Entities'() { when: GraphQLSchema schema = builder.getGraphQLSchema(); then: "Ensure the result is returned" schema; then: "Ensure that collections can be queried on" schema.getQueryType().getFieldDefinition("Droid").getArgument("appearsIn") then: "Ensure Subobjects may be queried upon" schema.getQueryType().getFieldDefinition("CodeList").getArguments().size() == 6 schema.getQueryType().getFieldDefinition("CodeList").getArgument("code").getType() == Scalars.GraphQLString } } ================================================ FILE: src/test/groovy/org/crygier/graphql/TestApplication.groovy ================================================ package org.crygier.graphql import groovy.transform.CompileStatic import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration @EnableAutoConfiguration @EntityScan @CompileStatic class TestApplication { public static void main(String[] args) { ApplicationContext ac = SpringApplication.run(TestApplication.class, args); } @Bean public GraphQLExecutor graphQLExecutor() { return new GraphQLExecutor(); } @Bean public GraphQlController() { return new GraphQlController(); } } ================================================ FILE: src/test/groovy/org/crygier/graphql/ThingQueryExecutorTest.groovy ================================================ package org.crygier.graphql import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.context.annotation.Configuration import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @Configuration @ContextConfiguration(loader = SpringBootContextLoader, classes = TestApplication) class ThingQueryExecutorTest extends Specification { @Autowired private GraphQLExecutor executor def 'Gets all things'() { given: def query = ''' query AllThingsQuery { Thing { id type } } ''' def expected = [ Thing: [ [ id: UUID.fromString("2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1"), type:'Thing1' ] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Query for thing by id'() { given: def query = ''' query ThingByIdQuery { Thing(id: "2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1") { id type } } ''' def expected = [ Thing: [ [ id: UUID.fromString("2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1"), type:'Thing1' ] ] ] when: def result = executor.execute(query).data then: result == expected } def 'Query with parameter'() { given: def query = ''' query ThingByIdQuery($id: UUID) { Thing(id: $id) { id type } } ''' def expected = [ Thing: [ [ id: UUID.fromString("2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1"), type:'Thing1' ] ] ] when: def result = executor.execute(query, [id: "2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1"]).data then: result == expected } def 'Query with alias'() { given: def query = ''' { t1: Thing(id: "2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1") { id type } } ''' def expected = [ t1: [ [ id: UUID.fromString("2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1"), type:'Thing1' ] ] ] when: def result = executor.execute(query).data then: result == expected } } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/collections/CollectionTest.java ================================================ package org.crygier.graphql.model.collections; import groovy.transform.CompileStatic; import org.crygier.graphql.annotation.SchemaDocumentation; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.Id; import java.util.ArrayList; import java.util.List; @Entity @CompileStatic public class CollectionTest { //testing that the Schema Builder does not break //when building collection of non-enum objects @Id @SchemaDocumentation("Primary Key for the CollectionTest Class") String id; @SchemaDocumentation("A List of Strings") @ElementCollection(targetClass=String.class) List codas = new ArrayList(); } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/embeddings/EmbeddingId.java ================================================ package org.crygier.graphql.model.embeddings; import org.crygier.graphql.model.starwars.Character; import org.crygier.graphql.model.starwars.Episode; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import java.io.Serializable; @Embeddable class EmbeddingId implements Serializable { @ManyToOne @JoinColumn(name = "character_id") private Character character; @Column(name = "episode") private Episode episode; @Column(name = "age") private int age; public EmbeddingId(Character character, Episode episode, int age) { this.character = character; this.episode = episode; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; EmbeddingId that = (EmbeddingId) o; if (age != that.age) return false; if (character != null ? !character.equals(that.character) : that.character != null) return false; return episode == that.episode; } @Override public int hashCode() { int result = character != null ? character.hashCode() : 0; result = 31 * result + (episode != null ? episode.hashCode() : 0); result = 31 * result + age; return result; } } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/embeddings/EmbeddingTest.java ================================================ package org.crygier.graphql.model.embeddings; import groovy.transform.CompileStatic; import org.crygier.graphql.annotation.GraphQLIgnore; import org.hibernate.annotations.Target; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Id; @Entity @CompileStatic public class EmbeddingTest { @EmbeddedId private EmbeddingId embeddingId; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/starwars/Character.groovy ================================================ package org.crygier.graphql.model.starwars import groovy.transform.CompileStatic import org.crygier.graphql.annotation.SchemaDocumentation import javax.persistence.* @Entity @SchemaDocumentation("Abstract representation of an entity in the Star Wars Universe") @CompileStatic abstract class Character { @Id @SchemaDocumentation("Primary Key for the Character Class") String id; @SchemaDocumentation("Name of the character") String name; @SchemaDocumentation("Who are the known friends to this character") @ManyToMany @JoinTable(name="character_friends", joinColumns=@JoinColumn(name="source_id", referencedColumnName="id"), inverseJoinColumns=@JoinColumn(name="friend_id", referencedColumnName="id")) Collection friends; @SchemaDocumentation("What Star Wars episodes does this character appear in") @ElementCollection(targetClass = Episode) @Enumerated(EnumType.ORDINAL) Collection appearsIn; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/starwars/CodeList.groovy ================================================ package org.crygier.graphql.model.starwars import groovy.transform.CompileStatic import org.crygier.graphql.annotation.SchemaDocumentation import javax.persistence.Entity import javax.persistence.Id import javax.persistence.JoinColumn import javax.persistence.ManyToOne @Entity @SchemaDocumentation("Database driven enumeration") @CompileStatic class CodeList { @Id @SchemaDocumentation("Primary Key for the Code List Class") Long id; String type; String code; Integer sequence; boolean active; String description; @ManyToOne @JoinColumn(name = "parent_id") CodeList parent; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/starwars/Droid.groovy ================================================ package org.crygier.graphql.model.starwars import groovy.transform.CompileStatic import org.crygier.graphql.annotation.GraphQLIgnore import org.crygier.graphql.annotation.SchemaDocumentation import javax.persistence.Entity @Entity @SchemaDocumentation("Represents an electromechanical robot in the Star Wars Universe") @CompileStatic class Droid extends Character { @SchemaDocumentation("Documents the primary purpose this droid serves") String primaryFunction; @GraphQLIgnore byte[] data; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/starwars/Episode.java ================================================ package org.crygier.graphql.model.starwars; public enum Episode { PHANTOM_MENACE, ATTACK_OF_THE_CLONES, REVENGE_OF_THE_SITH, A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/starwars/Human.groovy ================================================ package org.crygier.graphql.model.starwars import groovy.transform.CompileStatic import javax.persistence.Entity import javax.persistence.FetchType import javax.persistence.JoinColumn import javax.persistence.ManyToOne @Entity(name = "Human") @CompileStatic public class Human extends Character { String homePlanet; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "favorite_droid_id") Droid favoriteDroid; @ManyToOne @JoinColumn(name = "gender_code_id") CodeList gender; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/starwars/Spaceship.groovy ================================================ package org.crygier.graphql.model.starwars import javax.persistence.* import org.crygier.graphql.annotation.SchemaDocumentation import org.crygier.graphql.model.users.DateAndUser import groovy.transform.CompileStatic @Entity @SchemaDocumentation("Spaceships in the Star Wars Universe") @CompileStatic public class Spaceship { @Id @SchemaDocumentation("Primary Key for the Spaceship Class") public String id; @SchemaDocumentation("Name of the spaceship") String name; @Embedded @AttributeOverrides ([ @AttributeOverride(name="date",column=@Column(name="createddate")) ]) @AssociationOverrides ([ @AssociationOverride(name="user",joinColumns=@JoinColumn(name="createduser")) ]) public DateAndUser created; @Embedded @AttributeOverrides ([ @AttributeOverride(name="date",column=@Column(name="modifieddate")) ]) @AssociationOverrides ([ @AssociationOverride(name="user",joinColumns=@JoinColumn(name="modifieduser")) ]) DateAndUser modified; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/users/DateAndUser.groovy ================================================ package org.crygier.graphql.model.users import javax.persistence.Embeddable import javax.persistence.ManyToOne @Embeddable public class DateAndUser { public Date date; @ManyToOne public User user; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/users/User.groovy ================================================ package org.crygier.graphql.model.users import javax.persistence.Entity import javax.persistence.Id import org.crygier.graphql.annotation.SchemaDocumentation import groovy.transform.CompileStatic @Entity @SchemaDocumentation("User who uses the application") @CompileStatic class User { @Id @SchemaDocumentation("Primary Key for the User Class") String id; String firstName; String lastName; } ================================================ FILE: src/test/groovy/org/crygier/graphql/model/uuid/Thing.groovy ================================================ package org.crygier.graphql.model.uuid import groovy.transform.CompileStatic import org.crygier.graphql.annotation.SchemaDocumentation import javax.persistence.Entity import javax.persistence.Id @Entity @SchemaDocumentation("Database Thing with UUID field") @CompileStatic class Thing { @Id @SchemaDocumentation("Primary Key for the Thing Class") UUID id String type } ================================================ FILE: src/test/resources/application.yaml ================================================ spring: jpa: hibernate.ddl-auto: create-drop show-sql: true h2: console.enabled: true ================================================ FILE: src/test/resources/data.sql ================================================ -- Insert Code Lists insert into code_list (id, type, code, description, sequence, active) values (0, 'org.crygier.graphql.model.starwars.Gender', 'Male', 'Male', 1, true), (1, 'org.crygier.graphql.model.starwars.Gender', 'Female', 'Female', 2, true); -- Insert Droids insert into character (id, name, primary_function, dtype) values ('2000', 'C-3PO', 'Protocol', 'Droid'), ('2001', 'R2-D2', 'Astromech', 'Droid'); -- Insert Humans insert into character (id, name, home_planet, favorite_droid_id, dtype, gender_code_id) values ('1000', 'Luke Skywalker', 'Tatooine', '2000', 'Human', 0), ('1001', 'Darth Vader', 'Tatooine', '2001', 'Human', 0), ('1002', 'Han Solo', NULL, NULL, 'Human', 0), ('1003', 'Leia Organa', 'Alderaan', NULL, 'Human', 1), ('1004', 'Wilhuff Tarkin', NULL, NULL, 'Human', 0); -- Luke's friends insert into character_friends (source_id, friend_id) values ('1000', '1002'), ('1000', '1003'), ('1000', '2000'), ('1000', '2001'); -- Luke Appears in insert into character_appears_in (character_id, appears_in) values ('1000', 3), ('1000', 4), ('1000', 5), ('1000', 6); -- Vader's friends insert into character_friends (source_id, friend_id) values ('1001', '1004'); -- Vader Appears in insert into character_appears_in (character_id, appears_in) values ('1001', 3), ('1001', 4), ('1001', 5); -- Solo's friends insert into character_friends (source_id, friend_id) values ('1002', '1000'), ('1002', '1003'), ('1002', '2001'); -- Solo Appears in insert into character_appears_in (character_id, appears_in) values ('1002', 3), ('1002', 4), ('1002', 5), ('1002', 6); -- Leia's friends insert into character_friends (source_id, friend_id) values ('1003', '1000'), ('1003', '1002'), ('1003', '2000'), ('1003', '2001'); -- Leia Appears in insert into character_appears_in (character_id, appears_in) values ('1003', 3), ('1003', 4), ('1003', 5), ('1003', 6); -- Wilhuff's friends insert into character_friends (source_id, friend_id) values ('1004', '1001'); -- Wilhuff Appears in insert into character_appears_in (character_id, appears_in) values ('1004', 3); -- C3PO's friends insert into character_friends (source_id, friend_id) values ('2000', '1000'), ('2000', '1002'), ('2000', '1003'), ('2000', '2001'); -- C3PO Appears in insert into character_appears_in (character_id, appears_in) values ('2000', 3), ('2000', 4), ('2000', 5), ('2000', 6); -- R2's friends insert into character_friends (source_id, friend_id) values ('2001', '1000'), ('2001', '1002'), ('2001', '1003'); -- R2 Appears in insert into character_appears_in (character_id, appears_in) values ('2001', 3), ('2001', 4), ('2001', 5), ('2001', 6); -- Things insert into thing (id, type) values ('2D1EBC5B7D2741979CF0E84451C5BBB1', 'Thing1'); -- User insert into user(id, first_name, last_name) values ('1000','Bob', 'Austin'); insert into spaceship(id, name, createddate,createduser) values ('1000','X-Wing', sysdate, '1000'); ================================================ FILE: src/test/resources/static/index.html ================================================ Loading...