diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 7e63c1652..b86e00615 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -1603,12 +1603,26 @@ private Predicate getCompoundPredicate(CriteriaBuilder cb, List predi if (predicates.isEmpty()) return cb.disjunction(); if (predicates.size() == 1) { + if (logical == Logical.NOT) { + return cb.not(predicates.get(0)); + } return predicates.get(0); } - return (logical == Logical.OR) - ? cb.or(predicates.toArray(new Predicate[0])) - : cb.and(predicates.toArray(new Predicate[0])); + switch (logical) { + case OR: + return cb.or(predicates.toArray(new Predicate[0])); + case AND: + case EXISTS: + case NOT_EXISTS: + return cb.and(predicates.toArray(new Predicate[0])); + case NOT: + throw new RuntimeException("NOT expression cannot be applied to multiple predicates at once"); + default: + throw new RuntimeException( + "Unable to resolve applicable compound predicate for logical operand " + logical + ); + } } private PredicateFilter getPredicateFilter( diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 1500662b1..61709d3f5 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -722,6 +722,14 @@ private GraphQLArgument computeWhereArgument(ManagedType managedType) { .type(new GraphQLList(new GraphQLTypeReference(type))) .build() ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name(Logical.NOT.name()) + .description("Logical operation for expression") + .type(new GraphQLTypeReference(type)) + .build() + ) .field( GraphQLInputObjectField .newInputObjectField() @@ -815,6 +823,14 @@ private GraphQLInputObjectType computeSubqueryInputType(ManagedType managedTy .type(new GraphQLList(new GraphQLTypeReference(type))) .build() ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name(Logical.NOT.name()) + .description("Logical operation for expression") + .type(new GraphQLTypeReference(type)) + .build() + ) .field( GraphQLInputObjectField .newInputObjectField() @@ -896,6 +912,14 @@ private GraphQLInputObjectType computeWhereInputType(ManagedType managedType) .type(new GraphQLList(new GraphQLTypeReference(type))) .build() ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name(Logical.NOT.name()) + .description("Logical operation for expression") + .type(new GraphQLTypeReference(type)) + .build() + ) .field( GraphQLInputObjectField .newInputObjectField() @@ -1008,6 +1032,14 @@ private GraphQLInputType getWhereAttributeType(Attribute attribute) { .type(new GraphQLList(new GraphQLTypeReference(type))) .build() ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name(Logical.NOT.name()) + .description("Logical NOT criteria expression") + .type(new GraphQLTypeReference(type)) + .build() + ) .field( GraphQLInputObjectField .newInputObjectField() diff --git a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java index ffcb1c426..f5239f2ac 100644 --- a/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java +++ b/schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java @@ -23,6 +23,7 @@ enum Logical { AND, OR, + NOT, EXISTS, NOT_EXISTS; diff --git a/schema/src/test/java/com/introproventures/graphql/jpa/query/support/GraphQLExecutorTestsSupport.java b/schema/src/test/java/com/introproventures/graphql/jpa/query/support/GraphQLExecutorTestsSupport.java index 7499d959b..80d82a774 100644 --- a/schema/src/test/java/com/introproventures/graphql/jpa/query/support/GraphQLExecutorTestsSupport.java +++ b/schema/src/test/java/com/introproventures/graphql/jpa/query/support/GraphQLExecutorTestsSupport.java @@ -393,6 +393,24 @@ public void queryForEnumNe() { assertThat(result.toString()).isEqualTo(expected); } + @Test + public void queryForEnumNotEq() { + //given + String query = "{ Books(where: {genre: {NOT: {EQ:PLAY}}}) { select { id title, genre } }}"; + + 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).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryForEnumNin() { //given @@ -411,6 +429,24 @@ public void queryForEnumNin() { assertThat(result.toString()).isEqualTo(expected); } + @Test + public void queryForEnumNotIn() { + //given + String query = "{ Books(where: {genre: {NOT: {IN: PLAY}}}) { select { id title, genre } }}"; + + 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).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryForParentWithEnum() { //given @@ -467,6 +503,165 @@ public void queryAuthorBooksWithExplictOptional() { assertThat(result.toString()).isEqualTo(expected); } + @Test + public void queryAuthorBooksWithExplictOptionalNotLike() { + //given + String query = + "query { " + + "Authors(" + + " where: {" + + " books: {" + + " title: {NOT: {LIKE: \"Th\"}}" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + + " }" + + "}"; + + String expected = + "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=3, title=Anna Karenina, genre=NOVEL}, " + + "{id=2, title=War and Peace, genre=NOVEL}]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryAuthorBooksWithExplictOptionalNotLikeConjunction() { + //given + String query = + "query { " + + "Authors(" + + " where: {" + + " books: {" + + " AND: [{title: {NOT: {LIKE: \"War\"}}} {title: {NOT: {LIKE: \"Anna\"}}}]" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + + " }" + + "}"; + + String expected = + "{Authors={select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + + "{id=6, title=The Seagull, genre=PLAY}, " + + "{id=7, title=Three Sisters, genre=PLAY}" + + "]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryAuthorBooksWithNotOutsideOfLikeDisjunction() { + //should be the same result as queryAuthorBooksWithExplictOptionalNotLikeConjunction above + //due to logical inversion AND(Not Like A, Not Like B) => NOT(OR(Like a, Like b)) + //given + String query = + "query { " + + "Authors(" + + " where: {" + + " books: {" + + " NOT: {OR: [{title: {LIKE: \"War\"}} {title: {LIKE: \"Anna\"}}]}" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + + " }" + + "}"; + + String expected = + "{Authors={select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + + "{id=6, title=The Seagull, genre=PLAY}, " + + "{id=7, title=Three Sisters, genre=PLAY}" + + "]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryAuthorBooksWithNotOutsideOfConjunction() { + //given + String query = + "query { " + + "Authors(" + + " where: {" + + " books: {" + + " NOT: {AND: [{id: {GE: 4}} {genre: {EQ: PLAY}}]}" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + + " }" + + "}"; + + String expected = + "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=3, title=Anna Karenina, genre=NOVEL}, " + + "{id=2, title=War and Peace, genre=NOVEL}" + + "]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryAuthorBooksWithExplictOptionalEXISTS() { //given @@ -506,6 +701,51 @@ public void queryAuthorBooksWithExplictOptionalEXISTS() { assertThat(result.toString()).isEqualTo(expected); } + @Test + public void queryAuthorBooksWithExplictOptionalNotEXISTS() { + //given + String query = + "query { " + + "Authors(" + + " where: {" + + " NOT: {" + + " EXISTS: {" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + + " }" + + "}"; + + String expected = + "{Authors={select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=5, title=The Cherry Orchard, genre=PLAY}," + + " {id=6, title=The Seagull, genre=PLAY}," + + " {id=7, title=Three Sisters, genre=PLAY}" + + "]}, " + + "{id=8, name=Igor Dianov, books=[]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryAuthorBooksWithCollectionOrderBy() { //given @@ -2055,6 +2295,22 @@ public void ignoreOrder() { .contains(ErrorType.ValidationError, Arrays.asList("Books", "select", "description")); } + @Test + public void logicalNotOnlySupportsSingleOperand() { + //given + String query = "{ Books(where: {NOT: [{id: {EQ: 1}} {id: {EQ: 2}}]}){ select { id description } }}"; + + List result = executor.execute(query).getErrors(); + + //then + assertThat(result).hasSize(1); + assertThat(result.get(0)) + .isExactlyInstanceOf(ValidationError.class) + .extracting(ValidationError.class::cast) + .extracting("errorType", "queryPath") + .contains(ErrorType.ValidationError, Arrays.asList("Books")); + } + @Test public void titleOrder() { //given