diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java index 06ea2778c..ee92c789b 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -57,10 +58,12 @@ import com.introproventures.graphql.jpa.query.annotation.GraphQLDefaultOrderBy; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; + import graphql.GraphQLException; import graphql.execution.ValuesResolver; import graphql.language.Argument; import graphql.language.ArrayValue; +import graphql.language.AstValueHelper; import graphql.language.BooleanValue; import graphql.language.Comment; import graphql.language.EnumValue; @@ -79,6 +82,7 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentBuilder; +import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; @@ -378,12 +382,29 @@ protected Predicate getPredicate(CriteriaBuilder cb, Root from, From pat @SuppressWarnings( "unchecked" ) - private R getValue(Argument argument) { - return (R) argument.getValue(); + private > R getValue(Argument argument, DataFetchingEnvironment environment) { + Value value = argument.getValue(); + + if(VariableReference.class.isInstance(value)) { + String variableName = VariableReference.class.cast(value) + .getName(); + + Object variableValue = environment.getExecutionContext() + .getVariables() + .get(variableName); + + GraphQLArgument graphQLArgument = environment.getExecutionStepInfo() + .getFieldDefinition() + .getArgument(argument.getName()); + + return (R) AstValueHelper.astFromValue(variableValue, graphQLArgument.getType()); + } + + return (R) value; } protected Predicate getWherePredicate(CriteriaBuilder cb, Root root, From path, DataFetchingEnvironment environment, Argument argument) { - ObjectValue whereValue = getValue(argument); + ObjectValue whereValue = getValue(argument, environment); if(whereValue.getChildren().isEmpty()) return cb.conjunction(); @@ -404,7 +425,7 @@ protected Predicate getWherePredicate(CriteriaBuilder cb, Root root, From from, DataFetchingEnvironment environment, Argument argument) { - ObjectValue whereValue = getValue(argument); + ObjectValue whereValue = getValue(argument, environment); if (whereValue.getChildren().isEmpty()) return cb.disjunction(); @@ -494,7 +515,7 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, From path, DataFetchingEnvironment environment, Argument argument) { - ArrayValue whereValue = getValue(argument); + ArrayValue whereValue = getValue(argument, environment); if (whereValue.getValues().isEmpty()) return cb.disjunction(); @@ -896,22 +917,41 @@ else if (value instanceof VariableReference) { return argumentValue; } } else if (value instanceof ArrayValue) { - Object convertedValue = environment.getArgument(argument.getName()); - if (convertedValue != null && !getJavaType(environment, argument).isEnum()) { - // unwrap [[EnumValue{name='value'}]] - if(convertedValue instanceof Collection - && ((Collection) convertedValue).stream().allMatch(it->it instanceof Collection)) { - convertedValue = ((Collection) convertedValue).iterator().next(); + Collection arrayValue = environment.getArgument(argument.getName()); + + if (arrayValue != null) { + // Let's unwrap array of array values + if(arrayValue.stream() + .allMatch(it->it instanceof Collection)) { + arrayValue = Collection.class.cast(arrayValue.iterator() + .next()); } - - if(convertedValue instanceof Collection - && ((Collection) convertedValue).stream().anyMatch(it->it instanceof Value)) { - return ((Collection) convertedValue).stream() - .map((it) -> convertValue(environment, argument, (Value) it)) - .collect(Collectors.toList()); + + // Let's convert enum types, i.e. array of strings or EnumValue into Java type + if(getJavaType(environment, argument).isEnum()) { + Function objectValue = (obj) -> Value.class.isInstance(obj) + ? Value.class.cast(obj) + : new EnumValue(obj.toString()); + // Return real typed resolved array values converted into Java enums + return arrayValue.stream() + .map((it) -> convertValue(environment, + argument, + objectValue.apply(it))) + .collect(Collectors.toList()); + } + // Let's try handle Ast Value types + else if(arrayValue.stream() + .anyMatch(it->it instanceof Value)) { + return arrayValue.stream() + .map(it -> convertValue(environment, + argument, + Value.class.cast(it))) + .collect(Collectors.toList()); + } + // Return real typed resolved array value, i.e. Date, UUID, Long + else { + return arrayValue; } - // Return real typed resolved array value - return convertedValue; } else { // Wrap converted values in ArrayList return ((ArrayValue) value).getValues().stream() diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java index 8fc2713d5..17885d072 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java @@ -45,6 +45,14 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.Assert; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + +import graphql.ErrorType; +import graphql.ExecutionResult; +import graphql.GraphQLError; +import graphql.validation.ValidationError; + @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment=WebEnvironment.NONE) @@ -1440,4 +1448,5 @@ public void queryForTransientMethodAnnotatedWithGraphQLIgnoreShouldFail() { .extracting("validationErrorType", "queryPath") .containsOnly(tuple(ValidationErrorType.FieldUndefined, list("Books", "select", "authorName"))); } + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java new file mode 100644 index 000000000..b5ee97df3 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java @@ -0,0 +1,395 @@ +package com.introproventures.graphql.jpa.query.schema; + +import static com.introproventures.graphql.jpa.query.schema.model.book.Genre.NOVEL; +import static com.introproventures.graphql.jpa.query.schema.model.book.Genre.PLAY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.api.BDDAssertions.then; + +import java.io.IOException; +import java.util.Map; + +import javax.persistence.EntityManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + +import graphql.ExecutionResult; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@TestPropertySource({ "classpath:hibernate.properties" }) +public class GraphQLWhereVariableBindingsTests { + + @SpringBootApplication + static class Application { + + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + return new GraphQLJpaSchemaBuilder(entityManager) + .name("GraphQLBooks") + .description("Books JPA test schema"); + } + + } + + @Autowired + private GraphQLExecutor executor; + + @Test + public void queryWithSimpleEqualsVariableBinding() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"title\": {" + + " \"EQ\": \"War and Peace\"" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .hasSize(1) + .extracting("id", "title", "genre") + .containsOnly(tuple(2L, "War and Peace", NOVEL)); + } + + @Test + public void queryWithNestedWhereClauseVariableBinding() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " author {" + + " id" + + " name" + + " }" + + " title" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"author\": {" + + " \"name\": {" + + " \"EQ\": \"Leo Tolstoy\"" + + " }" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .hasSize(2) + .extracting("title") + .containsOnly("War and Peace", "Anna Karenina"); + then(result) + .extracting("Books") + .flatExtracting("select") + .hasSize(2) + .extracting("author") + .extracting("id", "name") + .containsOnly(tuple(1L, "Leo Tolstoy")); + } + + @Test + public void queryWithInVariableBinding() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"genre\": {" + + " \"IN\": [\"PLAY\"]" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .extracting("genre") + .containsOnly(PLAY); + } + + @Test + public void queryWithMultipleRestrictionForOneProperty() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"id\": {" + + " \"GE\": 5," + + " \"LE\": 7" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .extracting("id", "title") + .containsOnly( + tuple(5L, "The Cherry Orchard"), + tuple(6L, "The Seagull"), + tuple(7L, "Three Sisters") + ); + } + + @Test + public void queryWithPropertyWhereVariableBinding() throws IOException { + //given + String query = "" + + "query($booksWhereClause: BooksCriteriaExpression) {" + + " Authors {" + + " select {" + + " name" + + " books(where: $booksWhereClause) {" + + " genre" + + " }" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"booksWhereClause\": {" + + " \"genre\": {" + + " \"IN\": [\"NOVEL\"]" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Authors") + .flatExtracting("select") + .extracting("name") + .containsOnly("Leo Tolstoy"); + then(result) + .extracting("Authors") + .flatExtracting("select") + .flatExtracting("books") + .extracting("genre") + .containsOnly(NOVEL); + } + + @Test + public void queryWithRestrictionsForMultipleProperties() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"title\": {" + + " \"LIKE\": \"The\"" + + " }," + + " \"id\": {" + + " \"LT\": 6" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .extracting("id", "title") + .containsOnly(tuple(5L, "The Cherry Orchard")); + } + + @Test + public void queryBooksWithWhereVariableCriteriaEnumListExpression() throws IOException { + //given + String query = "query($where: BooksCriteriaExpression) {" + + " Books (where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + + String variables = "{" + + " \"where\": {" + + " \"genre\": {" + + " \"IN\": [\"NOVEL\"]" + + " }" + + " } " + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}}"; + + //when + Object result = executor.execute(query, getVariablesMap(variables)).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryBooksWithWhereVariableCriteriaEnumExpression() throws IOException { + //given + String query = "query($where: BooksCriteriaExpression) {" + + " Books (where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + + String variables = "{" + + " \"where\": {" + + " \"genre\": {" + + " \"IN\": \"NOVEL\"" + + " }" + + " } " + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}}"; + + //when + Object result = executor.execute(query, getVariablesMap(variables)).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryBooksWithWhereVariableCriteriaEQEnumExpression() throws IOException { + //given + String query = "query($where: BooksCriteriaExpression) {" + + " Books (where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + + String variables = "{" + + " \"where\": {" + + " \"genre\": {" + + " \"EQ\": \"NOVEL\"" + + " }" + + " } " + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}}"; + + //when + Object result = executor.execute(query, getVariablesMap(variables)).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @SuppressWarnings("unchecked") + private Map getVariablesMap(String variables) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + return (Map) mapper.readValue(variables, Map.class); + } +} \ No newline at end of file