diff --git a/pom.xml b/pom.xml index 8472ef5..49566d0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.intuit.graphql graphql-filter-java - 2.0.0 + 2.2.0 ${project.artifactId} A java library to transform graphql filter into database filter @@ -159,8 +159,8 @@ org.apache.maven.plugins maven-compiler-plugin - 8 - 8 + 17 + 17 diff --git a/src/main/java/com/intuit/graphql/filter/ast/AbstractExpression.java b/src/main/java/com/intuit/graphql/filter/ast/AbstractExpression.java index 3d2f0b5..d259f27 100644 --- a/src/main/java/com/intuit/graphql/filter/ast/AbstractExpression.java +++ b/src/main/java/com/intuit/graphql/filter/ast/AbstractExpression.java @@ -15,6 +15,8 @@ */ package com.intuit.graphql.filter.ast; +import java.util.Map; + /** * Base class for a node in the * expression tree. All concrete expression nodes @@ -27,6 +29,7 @@ public abstract class AbstractExpression implements Expression { private Expression leftOperand; private Operator operator; private Expression rightOperand; + private Map attributes; /** * Default constructor. @@ -110,4 +113,13 @@ public void setOperator(Operator operator) { public void setRightOperand(Expression rightOperand) { this.rightOperand = rightOperand; } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public Map getAttributes() { + return attributes; + } + } diff --git a/src/main/java/com/intuit/graphql/filter/ast/Operator.java b/src/main/java/com/intuit/graphql/filter/ast/Operator.java index 0484022..60cff9d 100644 --- a/src/main/java/com/intuit/graphql/filter/ast/Operator.java +++ b/src/main/java/com/intuit/graphql/filter/ast/Operator.java @@ -16,6 +16,8 @@ */ package com.intuit.graphql.filter.ast; +import java.util.Arrays; +import java.util.List; import java.util.Objects; /** @@ -26,17 +28,20 @@ */ public class Operator { private String key; - private String type; private Kind kind; + private List types; + private String type; - public Operator(String key, Kind kind, String... type) { + public Operator(String key, Kind kind, String... types) { this.key = key; this.kind = kind; - this.type = String.join("|", type); + this.types = Arrays.asList(types); + this.type = String.join("|", types); } public String getKey() { return key; } public String getType() { return type; } + public List getTypes() { return types; } public Kind getKind() { return kind; } public enum Kind { @@ -47,6 +52,7 @@ public enum Kind { public static final String TYPE_STRING = "String"; public static final String TYPE_NUMERIC = "Numeric"; public static final String TYPE_DATETIME = "DateTime"; + public static final String TYPE_JSON = "Json"; public static final Operator AND = new Operator("and", Operator.Kind.COMPOUND, TYPE_LOGICAL); public static final Operator OR = new Operator("or", Operator.Kind.COMPOUND, TYPE_LOGICAL); diff --git a/src/main/java/com/intuit/graphql/filter/client/FilterExpressionParser.java b/src/main/java/com/intuit/graphql/filter/client/FilterExpressionParser.java index 853b60e..2c3a809 100644 --- a/src/main/java/com/intuit/graphql/filter/client/FilterExpressionParser.java +++ b/src/main/java/com/intuit/graphql/filter/client/FilterExpressionParser.java @@ -15,6 +15,8 @@ */ package com.intuit.graphql.filter.client; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.intuit.graphql.filter.ast.BinaryExpression; import com.intuit.graphql.filter.ast.CompoundExpression; import com.intuit.graphql.filter.ast.Expression; @@ -48,13 +50,22 @@ public class FilterExpressionParser { private static final String KIND_UNARY = Operator.Kind.UNARY.name(); private final OperatorRegistry operatorRegistry; + private final ObjectMapper objectMapper; + public FilterExpressionParser() { this.operatorRegistry = OperatorRegistry.withDefaultOperators(); + this.objectMapper = new ObjectMapper(); } public FilterExpressionParser(OperatorRegistry operatorRegistry) { this.operatorRegistry = operatorRegistry; + this.objectMapper = new ObjectMapper(); + } + + public FilterExpressionParser(OperatorRegistry operatorRegistry, ObjectMapper objectMapper) { + this.operatorRegistry = operatorRegistry; + this.objectMapper = objectMapper; } public Expression parseFilterExpression(Map filterArgs) { @@ -102,8 +113,7 @@ private Expression handleFieldExpression(Map.Entry entry, final String key) { final ExpressionField leftOperand = new ExpressionField(entry.getKey().toString()); final BinaryExpression binaryExpression = (entry.getValue() instanceof Map) - ? (BinaryExpression) - createExpressionTree((Map) entry.getValue()) + ? (BinaryExpression) createExpressionTree((Map) entry.getValue()) : (BinaryExpression) handleBinary(entry, Operator.IN.getKey()); binaryExpression.setLeftOperand(leftOperand); @@ -115,21 +125,35 @@ private Expression handleUnary(Map.Entry entry, String key) { return new UnaryExpression(operand, getOperator(key), null); } - private Expression handleBinary(Map.Entry entry, String key) { + private Expression handleBinary(Map.Entry entry, String key) throws RuntimeException{ final BinaryExpression binaryExpression = new BinaryExpression(); - binaryExpression.setOperator(getOperator(key)); + final Operator operator = getOperator(key); + binaryExpression.setOperator(operator); + if (entry.getValue() instanceof Collection) { - final List expressionValues = new ArrayList<>(); - for (Comparable value : (List) entry.getValue()) { - expressionValues.add(convertIfDate(value)); + final List expressionValues = new ArrayList<>(); + for (Object value : (List) entry.getValue()) { + expressionValues.add(convertObject(value)); } binaryExpression.setRightOperand(new ExpressionValue(expressionValues)); - } else { - binaryExpression.setRightOperand(new ExpressionValue<>(convertIfDate((Comparable) entry.getValue()))); + return binaryExpression; + } else if (entry.getValue() instanceof Map) { + binaryExpression.setRightOperand(new ExpressionValue<>(getJson(entry))); + return binaryExpression; } + + binaryExpression.setRightOperand(new ExpressionValue<>(convertObject(entry.getValue()))); return binaryExpression; } + private String getJson(Map.Entry entry) { + try { + return objectMapper.writeValueAsString(entry.getValue()); + } catch (JsonProcessingException e) { + return ""; + } + } + private Expression handleCompound( Deque expressionStack, Map.Entry entry, String key) { @@ -164,6 +188,14 @@ private boolean isOperator(String key) { return false; } } + + private Object convertObject(Object value) { + if (value instanceof Comparable) { + return convertIfDate((Comparable) value); + } + return value; + } + private Comparable convertIfDate(Comparable value) { if (value == null) { return null; diff --git a/src/main/java/com/intuit/graphql/filter/visitors/ExpressionValueHandler.java b/src/main/java/com/intuit/graphql/filter/visitors/ExpressionValueHandler.java index 06225cd..e7752d0 100644 --- a/src/main/java/com/intuit/graphql/filter/visitors/ExpressionValueHandler.java +++ b/src/main/java/com/intuit/graphql/filter/visitors/ExpressionValueHandler.java @@ -7,5 +7,5 @@ public interface ExpressionValueHandler { void handle( Operator operator, StringBuilder expressionBuilder, - ExpressionValue expressionValue); + ExpressionValue expressionValue); } \ No newline at end of file diff --git a/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionValueVisitor.java b/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionValueVisitor.java index 872b827..f3aaf01 100644 --- a/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionValueVisitor.java +++ b/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionValueVisitor.java @@ -26,7 +26,7 @@ public SQLExpressionValueVisitor(Map expressio } public String visitExpressionValue( - Operator operator, ExpressionValue expressionValue, String data) { + Operator operator, ExpressionValue expressionValue, String data) { final StringBuilder expressionBuilder = new StringBuilder(data); final ExpressionValueHandler handler = expressionValueHandlers.get(operator); @@ -43,7 +43,7 @@ public Map getExpressionValueHandlers() { return expressionValueHandlers; } - protected String resolveValue(Comparable value) { + protected String resolveValue(Object value) { if (value instanceof Number) { return value.toString(); } else { @@ -56,7 +56,7 @@ public class ContainsHandler implements ExpressionValueHandler { public void handle( Operator operator, StringBuilder expressionBuilder, - ExpressionValue expressionValue) { + ExpressionValue expressionValue) { final String value = expressionValue.infix(); if (hasWildcardValue(value)) { @@ -76,7 +76,7 @@ public class StartsHandler implements ExpressionValueHandler { public void handle( Operator operator, StringBuilder expressionBuilder, - ExpressionValue expressionValue) { + ExpressionValue expressionValue) { expressionBuilder.append("'").append(expressionValue.infix()).append("%").append("'"); } @@ -87,7 +87,7 @@ public class EndsHandler implements ExpressionValueHandler { public void handle( Operator operator, StringBuilder expressionBuilder, - ExpressionValue expressionValue) { + ExpressionValue expressionValue) { expressionBuilder.append("'").append("%").append(expressionValue.infix()).append("'"); } @@ -98,9 +98,9 @@ public class BetweenHandler implements ExpressionValueHandler { public void handle( Operator operator, StringBuilder expressionBuilder, - ExpressionValue expressionValue) { + ExpressionValue expressionValue) { - List expressionValues = (List)expressionValue.value(); + List expressionValues = (List)expressionValue.value(); expressionBuilder .append(resolveValue(expressionValues.get(0))) @@ -114,9 +114,9 @@ public class InHandler implements ExpressionValueHandler { public void handle( Operator operator, StringBuilder expressionBuilder, - ExpressionValue expressionValue) { + ExpressionValue expressionValue) { - final List expressionValues = (List)expressionValue.value(); + final List expressionValues = (List)expressionValue.value(); expressionBuilder.append("("); for (int i = 0; i < expressionValues.size(); i++) { diff --git a/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionVisitor.java b/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionVisitor.java index 15d4f36..140c1af 100644 --- a/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionVisitor.java +++ b/src/main/java/com/intuit/graphql/filter/visitors/SQLExpressionVisitor.java @@ -1,5 +1,7 @@ package com.intuit.graphql.filter.visitors; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.intuit.graphql.filter.ast.BinaryExpression; import com.intuit.graphql.filter.ast.CompoundExpression; import com.intuit.graphql.filter.ast.Expression; @@ -63,6 +65,7 @@ public class SQLExpressionVisitor implements ExpressionVisitor { private Map mappings; private Map> metadataCollector; private SqlQueryValueNormalizer sqlQueryValueNormalizer; + private ObjectMapper objectMapper; private boolean generateWherePrefix = true; private String metadataPrefix = DEFAULT_METADATA_PREFIX; @@ -76,6 +79,7 @@ public SQLExpressionVisitor(final Map fieldMap) { this.expressionValueVisitor = SQLExpressionValueVisitor.DEFAULT; this.fieldValueTransformer = DEFAULT_FIELD_VALUE_TRANSFORMER; this.sqlQueryValueNormalizer = DEFAULT_NORMALIZER; + this.objectMapper = new ObjectMapper(); } public SQLExpressionVisitor( @@ -130,7 +134,7 @@ public String visitBinaryExpression(final BinaryExpression binaryExpression, fin operatorStack.push(binaryExpression.getOperator()); final String rightOperand = binaryExpression.getRightOperand().accept(this, ""); - final String[] filterValues = rightOperand.replaceAll("[()]", "").split(","); + final String[] filterValues = rightOperand.replaceAll("[(){}]", "").split(","); collectMetadata(leftOperand, filterValues); if (customExpressionResolver.contains(leftOperand, binaryExpression.getOperator())) { @@ -228,6 +232,12 @@ private String prepareCustomExpression( binaryExpression, fieldName, queryString, resolvedOperator); } + if (isJsonQueryString(queryString)) { + binaryExpression.setAttributes(stringAsJson(queryString)); + return customExpression.generateExpression( + binaryExpression, fieldName, queryString, resolvedOperator); + } + final String enclosingLogicalOperator = customExpression.getEnclosingLogicalOperator().getValue(); return Arrays.stream(filterValues) @@ -236,6 +246,15 @@ binaryExpression, fieldName, normalizeString(filterValue), resolvedOperator)) .collect(Collectors.joining(" " + enclosingLogicalOperator + " ")); } + private Map stringAsJson(String queryString) { + final String query = queryString.substring(1, queryString.length()-1); + try { + return objectMapper.readValue(query, Map.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + private String formatBinaryExpression( String data, String leftOperand, @@ -275,6 +294,10 @@ private void collectMetadata(final String metaDataType, final String[] filterVal metadataCollector.put(metadataPrefix + metaDataType, filterValueList); } + private boolean isJsonQueryString(String queryString) { + return queryString.startsWith("'{") && queryString.endsWith("}'"); + } + public String resolveOperator(Operator operator) { return mappings.getOrDefault(operator, ""); } @@ -334,4 +357,12 @@ public CustomExpressionResolver getCustomExpressionResolver() { public void setCustomExpressionResolver(CustomExpressionResolver customExpressionResolver) { this.customExpressionResolver = customExpressionResolver; } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } } diff --git a/src/test/java/com/intuit/graphql/filter/common/TestConstants.java b/src/test/java/com/intuit/graphql/filter/common/TestConstants.java index 5f5f262..c91c8b0 100644 --- a/src/test/java/com/intuit/graphql/filter/common/TestConstants.java +++ b/src/test/java/com/intuit/graphql/filter/common/TestConstants.java @@ -133,6 +133,16 @@ public class TestConstants { " }\n" + "}"; + public static final String BIRTH_DATE_BETWEEN = "{\n" + + " searchEmployees(filter: {\n" + + " birthDate: {between: [\"1996-12-19T16:39:57-08:00\", \"2024-12-19T16:39:57-08:00\"]}\n" + + " })\n" + + " {\n" + + " firstName\n" + + " lastName\n" + + " }\n" + + "}"; + public static final String FILTER_WITH_OTHER_ARGS = "{\n" + " searchEmployees (id: \"123\", filter : {\n" + " firstName : {equals: \"Saurabh\"}\n" + @@ -185,6 +195,16 @@ public class TestConstants { " }\n" + "}"; + public static final String AGE_BETWEEN = "{\n" + + " searchEmployees(filter: {\n" + + " age: {between: [32, 40]}\n" + + " })\n" + + " {\n" + + " firstName\n" + + " lastName\n" + + " }\n" + + "}"; + public static final String INVALID_FILTER = "{\n" + " searchEmployees(filter: {\n" + " firstName : {contains: \"Saurabh\"}\n" + diff --git a/src/test/java/com/intuit/graphql/filter/visitors/SQLExpressionTest.java b/src/test/java/com/intuit/graphql/filter/visitors/SQLExpressionTest.java index 79ed394..0a42026 100644 --- a/src/test/java/com/intuit/graphql/filter/visitors/SQLExpressionTest.java +++ b/src/test/java/com/intuit/graphql/filter/visitors/SQLExpressionTest.java @@ -147,6 +147,25 @@ public void filterExpressionWithAgeIn () { Assert.assertEquals(expectedExpression,getEmployeeDataFetcher().getSqlExpression()); } + + @Test + public void filterExpressionWithAgeBetween () { + ExecutionResult result = getGraphQL().execute(TestConstants.AGE_BETWEEN); + + String expectedExpression = "WHERE (age BETWEEN 32 AND 40)"; + + Assert.assertEquals(expectedExpression,getEmployeeDataFetcher().getSqlExpression()); + } + + @Test + public void filterExpressionWithBirthDateBetween () { + ExecutionResult result = getGraphQL().execute(TestConstants.BIRTH_DATE_BETWEEN); + + String expectedExpression = "WHERE (birthDate BETWEEN '1996-12-20T00:39:57Z' AND '2024-12-20T00:39:57Z')"; + + Assert.assertEquals(expectedExpression,getEmployeeDataFetcher().getSqlExpression()); + } + @Test public void invalidFilterExpression () { ExecutionResult result = getGraphQL().execute(TestConstants.INVALID_FILTER); @@ -210,4 +229,6 @@ public void compoundFilterExpressionWithEnds() { Assert.assertEquals(expectedExpression,getEmployeeDataFetcher().getSqlExpression()); } + + } diff --git a/src/test/resources/schema.graphql b/src/test/resources/schema.graphql index c10fd60..d77b0dc 100644 --- a/src/test/resources/schema.graphql +++ b/src/test/resources/schema.graphql @@ -43,6 +43,7 @@ input IntExpression { lt: Int lte: Int in: [Int!] + between: [Int!] } input DateExpression { @@ -51,4 +52,5 @@ input DateExpression { gte: DateTime lt: DateTime lte: DateTime + between: [DateTime!] } \ No newline at end of file