From d44cf334a19adc57416e10b117c8290c476925bc Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 10:48:58 +0700 Subject: [PATCH 01/10] version 1.5.4 --- .../graphql/scheme/GraphQLSchemaParser.java | 93 ++++++++++++++++--- .../test/scheme/GraphQLSchemaParserTest.java | 55 +++++++++++ 2 files changed, 133 insertions(+), 15 deletions(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java index 99324372..922c4ff7 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java @@ -9,6 +9,7 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.List; import java.util.Map; import static com.tvd12.ezyfox.io.EzyStrings.EMPTY_STRING; @@ -16,6 +17,9 @@ @AllArgsConstructor public final class GraphQLSchemaParser { + private static final String VARIABLE_PLACEHOLDER_FIELD = + "__ezyhttp_graphql_variable__"; + private final ObjectMapper objectMapper; @SuppressWarnings({"unchecked", "MethodLength"}) @@ -66,12 +70,12 @@ public GraphQLSchema parseQuery( ); String arguments = "{" + argumentsBuilder + "}"; try { - childBuilder.arguments( - objectMapper.readValue( - arguments, - Map.class - ) + Map argumentMap = objectMapper.readValue( + arguments, + Map.class ); + replaceVariablePlaceholders(argumentMap, variables); + childBuilder.arguments(argumentMap); } catch (Exception e) { throw new GraphQLObjectMapperException( EzyMapBuilder.mapBuilder() @@ -270,22 +274,19 @@ private int extractQueryArguments( StringBuilder varNameBuilder = new StringBuilder(); for (++i; i < queryLength; ++i) { ch = query.charAt(i); - if (ch == ' ') { - continue; - } - if (ch != ',' && ch != ')' && ch != '}') { + if (isGraphQLNameChar(ch)) { varNameBuilder.append(ch); } else { --i; break; } } - String varName = varNameBuilder.toString(); - Object value = variables.get(varName); - if (value instanceof String) { - value = "\"" + value + "\""; - } - argumentsBuilder.append(value); + argumentsBuilder + .append("{\"") + .append(VARIABLE_PLACEHOLDER_FIELD) + .append("\":\"") + .append(varNameBuilder) + .append("\"}"); continue; } argumentsBuilder.append(ch); @@ -293,6 +294,68 @@ private int extractQueryArguments( return i; } + @SuppressWarnings("unchecked") + private void replaceVariablePlaceholders( + Map arguments, + Map variables + ) { + if (arguments == null || variables == null) { + return; + } + Deque stack = new ArrayDeque<>(); + stack.push(arguments); + while (!stack.isEmpty()) { + Object item = stack.pop(); + if (item instanceof Map) { + Map map = (Map) item; + for (Map.Entry entry : map.entrySet()) { + Object value = entry.getValue(); + if (isVariablePlaceholder(value)) { + entry.setValue(getVariableValue(value, variables)); + } else if (value instanceof Map || value instanceof List) { + stack.push(value); + } + } + } else if (item instanceof List) { + List list = (List) item; + int size = list.size(); + for (int i = 0; i < size; ++i) { + Object value = list.get(i); + if (isVariablePlaceholder(value)) { + list.set(i, getVariableValue(value, variables)); + } else if (value instanceof Map || value instanceof List) { + stack.push(value); + } + } + } + } + } + + private boolean isVariablePlaceholder(Object value) { + return value instanceof Map + && ((Map) value).size() == 1 + && ((Map) value).containsKey(VARIABLE_PLACEHOLDER_FIELD); + } + + private Object getVariableValue( + Object placeholder, + Map variables + ) { + Object variableName = ((Map) placeholder).get( + VARIABLE_PLACEHOLDER_FIELD + ); + return variableName instanceof String + ? variables.get(variableName) + : null; + } + + private boolean isGraphQLNameChar(char ch) { + return (ch >= 'A' && ch <= 'Z') + || (ch >= 'a' && ch <= 'z') + || (ch >= '0' && ch <= '9') + || ch == '_'; + } + private GraphQLField.Builder peekFieldStackItemOrThrow( Deque stack, String message diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java index 0f9ac568..8c63a225 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java @@ -367,6 +367,61 @@ public void parseQueryNormalWithArgsSpecialCase() { ); } + @Test + public void parseQueryWithStringVariableSpecialChars() { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + + // when + GraphQLSchema schema = instance.parseQuery( + "{me(name: $name){}}", + Collections.singletonMap("name", "he\"l\\lo\nworld") + ); + + // then + Asserts.assertEquals( + schema.getQueryDefinitions().get(0).getArguments(), + EzyMapBuilder.mapBuilder() + .put("name", "he\"l\\lo\nworld") + .toMap(), + false + ); + } + + @Test + public void parseQueryWithVariablesInListAndNestedObject() { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + + // when + GraphQLSchema schema = instance.parseQuery( + "{me(ids: [$id], filter: {name: $name}){}}", + EzyMapBuilder.mapBuilder() + .put("id", 1) + .put("name", "Dzung") + .toMap() + ); + + // then + Asserts.assertEquals( + schema.getQueryDefinitions().get(0).getArguments(), + EzyMapBuilder.mapBuilder() + .put("ids", Collections.singletonList(1)) + .put( + "filter", + EzyMapBuilder.mapBuilder() + .put("name", "Dzung") + .toMap() + ) + .toMap(), + false + ); + } + @Test public void parseQueryInvalidArgumentTest() { // given From 183d7165068ef51473473170f61fc510a54a80fc Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 10:53:16 +0700 Subject: [PATCH 02/10] version 1.5.4 --- .../graphql/scheme/GraphQLSchemaParser.java | 41 ++++++++++-- .../test/scheme/GraphQLSchemaParserTest.java | 67 +++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java index 922c4ff7..819c0584 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java @@ -17,11 +17,11 @@ @AllArgsConstructor public final class GraphQLSchemaParser { + private final ObjectMapper objectMapper; + private static final String VARIABLE_PLACEHOLDER_FIELD = "__ezyhttp_graphql_variable__"; - private final ObjectMapper objectMapper; - @SuppressWarnings({"unchecked", "MethodLength"}) public GraphQLSchema parseQuery( String queryToParse, @@ -245,12 +245,45 @@ private StringBuilder backwardStandardize(String query) { private String removeQueryPrefix(String s) { String prefix = "query"; - if (s.startsWith(prefix)) { - return s.substring(prefix.length()); + if (s.startsWith(prefix) + && (s.length() == prefix.length() + || !isGraphQLNameChar(s.charAt(prefix.length())))) { + int selectionStart = findOperationSelectionStart( + s, + prefix.length() + ); + return selectionStart >= 0 + ? s.substring(selectionStart) + : s.substring(prefix.length()); } return s; } + private int findOperationSelectionStart(String s, int start) { + int parenthesesCount = 0; + int quoteCount = 0; + int quotesCount = 0; + int length = s.length(); + for (int i = start; i < length; ++i) { + char prevCh = s.charAt(i - 1); + char ch = s.charAt(i); + if (prevCh != '\\' && ch == '\'') { + quoteCount = quoteCount == 0 ? 1 : 0; + } else if (prevCh != '\\' && ch == '"') { + quotesCount = quotesCount == 0 ? 1 : 0; + } else if (quoteCount == 0 && quotesCount == 0) { + if (ch == '(') { + ++parenthesesCount; + } else if (ch == ')') { + --parenthesesCount; + } else if (ch == '{' && parenthesesCount == 0) { + return i; + } + } + } + return -1; + } + private int extractQueryArguments( StringBuilder argumentsBuilder, String query, diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java index 8c63a225..bb1c68a8 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java @@ -422,6 +422,73 @@ public void parseQueryWithVariablesInListAndNestedObject() { ); } + @Test + public void parseQuerySubmitWorkReportWithOperationVariables() { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + String contentJson = "{\"items\":[{\"title\":\"Done\"}]}"; + String scheduleJson = "[{\"time\":\"09:00\",\"task\":\"Daily\"}]"; + String query = + "query($reportType: String, $reportDate: String, " + + "$employeeName: String, $username: String, " + + "$email: String, $department: String, " + + "$contentJson: String, $scheduleJson: String) {\n" + + " submit_work_report(" + + "reportType: $reportType, " + + "reportDate: $reportDate, " + + "employeeName: $employeeName, " + + "username: $username, " + + "email: $email, " + + "department: $department, " + + "contentJson: $contentJson, " + + "scheduleJson: $scheduleJson" + + ") {\n" + + " success postId slug message\n" + + " }\n" + + "}"; + + // when + GraphQLSchema schema = instance.parseQuery( + query, + EzyMapBuilder.mapBuilder() + .put("reportType", "daily") + .put("reportDate", "2026-06-26") + .put("employeeName", "Dzung") + .put("username", "dzung@example.com") + .put("email", "dzung@example.com") + .put("department", "") + .put("contentJson", contentJson) + .put("scheduleJson", scheduleJson) + .toMap() + ); + + // then + GraphQLField queryDefinition = schema.getQueryDefinitions().get(0); + Asserts.assertEquals(schema.getQueryDefinitions().size(), 1); + Asserts.assertEquals(queryDefinition.getName(), "submit_work_report"); + Asserts.assertEquals( + queryDefinition.getArguments(), + EzyMapBuilder.mapBuilder() + .put("reportType", "daily") + .put("reportDate", "2026-06-26") + .put("employeeName", "Dzung") + .put("username", "dzung@example.com") + .put("email", "dzung@example.com") + .put("department", "") + .put("contentJson", contentJson) + .put("scheduleJson", scheduleJson) + .toMap(), + false + ); + Asserts.assertEquals(queryDefinition.getFields().size(), 4); + Asserts.assertEquals(queryDefinition.getFields().get(0).getName(), "success"); + Asserts.assertEquals(queryDefinition.getFields().get(1).getName(), "postId"); + Asserts.assertEquals(queryDefinition.getFields().get(2).getName(), "slug"); + Asserts.assertEquals(queryDefinition.getFields().get(3).getName(), "message"); + } + @Test public void parseQueryInvalidArgumentTest() { // given From b5780272b89c3f588036198292eb4899ea1d1e15 Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 10:57:10 +0700 Subject: [PATCH 03/10] version 1.5.4 --- .../graphql/data/GraphQLDataFilter.java | 90 +++++++++++++++---- .../server/graphql/data/GraphQLField.java | 72 +++++++++++++-- 2 files changed, 138 insertions(+), 24 deletions(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java index f0a1230c..af04762d 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java @@ -22,23 +22,18 @@ public Map filter( GraphQLField queryDefinition ) { Map answer = new HashMap<>(); - Map parentMap = null; Deque stack = new ArrayDeque<>(); - stack.push(new StackEntry(queryDefinition, data)); + stack.push(new StackEntry(queryDefinition, data, answer)); while (!stack.isEmpty()) { StackEntry entry = stack.pop(); - String parentName = entry.field.getName(); - parentMap = parentMap == null - ? answer - : (Map) parentMap.get(parentName); GraphQLField allField = entry.field.getField(ALL_FIELDS); - if (allField != null) { + if (allField != null && entry.data != null) { Set entries = entry.data.entrySet(); for (Map.Entry e : entries) { Object v = e.getValue(); if (v != null) { - parentMap.put(e.getKey(), v); + entry.output.put(e.getKey(), v); } } } @@ -53,18 +48,17 @@ public Map filter( continue; } if (field.getFields().isEmpty()) { - parentMap.put(fieldName, value); + entry.output.put(fieldName, value); continue; } if (value instanceof Map) { - Object newItem = new HashMap<>(); - parentMap.put(fieldName, newItem); - stack.push(new StackEntry(field, (Map) value)); + Map newItem = new HashMap<>(); + entry.output.put(fieldName, newItem); + stack.push(new StackEntry(field, (Map) value, newItem)); } else if (value instanceof List) { - parentMap.put( - fieldName, - filterList((List) value, field) - ); + List newList = new LinkedList<>(); + entry.output.put(fieldName, newList); + pushListItems(stack, (List) value, field, newList); } else { throw new GraphQLInvalidSchemeException( EzyMapBuilder.mapBuilder() @@ -84,16 +78,76 @@ public List filterList( GraphQLField queryDefinition ) { List answer = new LinkedList<>(); - for (Map map : dataList) { - answer.add(filter(map, queryDefinition)); + Deque stack = new ArrayDeque<>(); + pushListItems(stack, dataList, queryDefinition, answer); + while (!stack.isEmpty()) { + StackEntry entry = stack.pop(); + + GraphQLField allField = entry.field.getField(ALL_FIELDS); + if (allField != null && entry.data != null) { + Set entries = entry.data.entrySet(); + for (Map.Entry e : entries) { + Object v = e.getValue(); + if (v != null) { + entry.output.put(e.getKey(), v); + } + } + } + + for (GraphQLField field : entry.field.getFields()) { + String fieldName = field.getName(); + if (entry.data == null) { + continue; + } + Object value = entry.data.get(fieldName); + if (value == null) { + continue; + } + if (field.getFields().isEmpty()) { + entry.output.put(fieldName, value); + continue; + } + if (value instanceof Map) { + Map newItem = new HashMap<>(); + entry.output.put(fieldName, newItem); + stack.push(new StackEntry(field, (Map) value, newItem)); + } else if (value instanceof List) { + List newList = new LinkedList<>(); + entry.output.put(fieldName, newList); + pushListItems(stack, (List) value, field, newList); + } else { + throw new GraphQLInvalidSchemeException( + EzyMapBuilder.mapBuilder() + .put("schema", "invalid") + .put("field", fieldName) + .toMap() + ); + } + } } return answer; } + @SuppressWarnings({"rawtypes", "unchecked"}) + private void pushListItems( + Deque stack, + List dataList, + GraphQLField queryDefinition, + List answer + ) { + int size = dataList.size(); + for (int i = size - 1; i >= 0; --i) { + Map item = new HashMap<>(); + answer.add(0, item); + stack.push(new StackEntry(queryDefinition, dataList.get(i), item)); + } + } + @AllArgsConstructor @SuppressWarnings("rawtypes") private static class StackEntry { private GraphQLField field; private Map data; + private Map output; } } diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java index 6ff81232..85f56f19 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java @@ -5,8 +5,10 @@ import com.tvd12.ezyhttp.core.codec.SingletonStringDeserializer; import lombok.Getter; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.List; import java.util.Map; @@ -162,15 +164,48 @@ private static String toString( Map arguments, List fields ) { - StringBuilder builder = new StringBuilder() - .append(name); + StringBuilder builder = new StringBuilder(); + Deque stack = new ArrayDeque<>(); + stack.push(new ToStringTask(name, arguments, fields)); + while (!stack.isEmpty()) { + ToStringTask task = stack.pop(); + if (task.text != null) { + builder.append(task.text); + continue; + } + appendFieldStart(builder, task.name, task.arguments); + List taskFields = task.fields; + if (taskFields == null) { + continue; + } + builder.append(", ["); + stack.push(new ToStringTask("]")); + for (int i = taskFields.size() - 1; i >= 0; --i) { + GraphQLField field = taskFields.get(i); + stack.push( + new ToStringTask( + field.name, + field.arguments, + field.fields + ) + ); + if (i > 0) { + stack.push(new ToStringTask(", ")); + } + } + } + return builder.toString(); + } + + private static void appendFieldStart( + StringBuilder builder, + String name, + Map arguments + ) { + builder.append(name); if (arguments != null && !arguments.isEmpty()) { builder.append("(").append(arguments).append(")"); } - if (fields != null) { - builder.append(", ").append(fields); - } - return builder.toString(); } @Override @@ -226,4 +261,29 @@ public String toString() { return GraphQLField.toString(name, arguments, fields); } } + + private static class ToStringTask { + private final String text; + private final String name; + private final Map arguments; + private final List fields; + + ToStringTask(String text) { + this.text = text; + this.name = null; + this.arguments = null; + this.fields = null; + } + + ToStringTask( + String name, + Map arguments, + List fields + ) { + this.text = null; + this.name = name; + this.arguments = arguments; + this.fields = fields; + } + } } From cd588cc31cc0c07983f5b009d2586a3d0eea600f Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 10:59:13 +0700 Subject: [PATCH 04/10] update --- .../graphql/data/GraphQLDataFilter.java | 53 +------ .../test/data/GraphQLDataFilterTest.java | 146 ++++++++++++++++++ 2 files changed, 153 insertions(+), 46 deletions(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java index af04762d..6c24c842 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java @@ -24,51 +24,7 @@ public Map filter( Map answer = new HashMap<>(); Deque stack = new ArrayDeque<>(); stack.push(new StackEntry(queryDefinition, data, answer)); - while (!stack.isEmpty()) { - StackEntry entry = stack.pop(); - - GraphQLField allField = entry.field.getField(ALL_FIELDS); - if (allField != null && entry.data != null) { - Set entries = entry.data.entrySet(); - for (Map.Entry e : entries) { - Object v = e.getValue(); - if (v != null) { - entry.output.put(e.getKey(), v); - } - } - } - - for (GraphQLField field : entry.field.getFields()) { - String fieldName = field.getName(); - if (entry.data == null) { - continue; - } - Object value = entry.data.get(fieldName); - if (value == null) { - continue; - } - if (field.getFields().isEmpty()) { - entry.output.put(fieldName, value); - continue; - } - if (value instanceof Map) { - Map newItem = new HashMap<>(); - entry.output.put(fieldName, newItem); - stack.push(new StackEntry(field, (Map) value, newItem)); - } else if (value instanceof List) { - List newList = new LinkedList<>(); - entry.output.put(fieldName, newList); - pushListItems(stack, (List) value, field, newList); - } else { - throw new GraphQLInvalidSchemeException( - EzyMapBuilder.mapBuilder() - .put("schema", "invalid") - .put("field", fieldName) - .toMap() - ); - } - } - } + filterStack(stack); return answer; } @@ -80,6 +36,12 @@ public List filterList( List answer = new LinkedList<>(); Deque stack = new ArrayDeque<>(); pushListItems(stack, dataList, queryDefinition, answer); + filterStack(stack); + return answer; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void filterStack(Deque stack) { while (!stack.isEmpty()) { StackEntry entry = stack.pop(); @@ -125,7 +87,6 @@ public List filterList( } } } - return answer; } @SuppressWarnings({"rawtypes", "unchecked"}) diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java index 3dce23cf..489f9945 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java @@ -5,9 +5,14 @@ import com.tvd12.test.assertion.Asserts; import org.testng.annotations.Test; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static com.tvd12.ezyfox.util.EzyMapBuilder.mapBuilder; +import static com.tvd12.ezyhttp.server.graphql.constants.GraphQLConstants.ALL_FIELDS; + public class GraphQLDataFilterTest { private final GraphQLDataFilter instance = new GraphQLDataFilter(); @@ -29,4 +34,145 @@ public void filterWithNullDataTest() { // then Asserts.assertEquals(result, Collections.emptyMap(), false); } + + @Test + public void filterWithAllFieldsTest() { + // given + GraphQLField queryDefinition = GraphQLField.builder() + .name("user") + .addField( + GraphQLField.builder() + .name(ALL_FIELDS) + .build() + ) + .build(); + Map data = mapBuilder() + .put("id", 1) + .put("name", "Dzung") + .put("nullable", null) + .toMap(); + + // when + Map result = instance.filter(data, queryDefinition); + + // then + Asserts.assertEquals( + result, + mapBuilder() + .put("id", 1) + .put("name", "Dzung") + .toMap(), + false + ); + } + + @Test + public void filterWithAllFieldsAndNestedOverrideTest() { + // given + GraphQLField queryDefinition = GraphQLField.builder() + .name("user") + .addField( + GraphQLField.builder() + .name(ALL_FIELDS) + .build() + ) + .addField( + GraphQLField.builder() + .name("friends") + .addField( + GraphQLField.builder() + .name("name") + .build() + ) + .build() + ) + .build(); + Map data = mapBuilder() + .put("id", 1) + .put( + "friends", + Arrays.asList( + mapBuilder() + .put("id", 2) + .put("name", "Foo") + .toMap(), + mapBuilder() + .put("id", 3) + .put("name", "Bar") + .toMap() + ) + ) + .toMap(); + + // when + Map result = instance.filter(data, queryDefinition); + + // then + Asserts.assertEquals( + result, + mapBuilder() + .put("id", 1) + .put( + "friends", + Arrays.asList( + mapBuilder() + .put("name", "Foo") + .toMap(), + mapBuilder() + .put("name", "Bar") + .toMap() + ) + ) + .toMap(), + false + ); + } + + @Test + public void filterListWithAllFieldsTest() { + // given + GraphQLField queryDefinition = GraphQLField.builder() + .name("friends") + .addField( + GraphQLField.builder() + .name(ALL_FIELDS) + .build() + ) + .build(); + + // when + Object result = instance.filterList( + Arrays.asList( + new HashMap<>( + mapBuilder() + .put("id", 1) + .put("name", "Foo") + .toMap() + ), + new HashMap<>( + mapBuilder() + .put("id", 2) + .put("name", "Bar") + .toMap() + ) + ), + queryDefinition + ); + + // then + Asserts.assertEquals( + result, + Arrays.asList( + mapBuilder() + .put("id", 1) + .put("name", "Foo") + .toMap(), + mapBuilder() + .put("id", 2) + .put("name", "Bar") + .toMap() + ), + false + ); + } } From 8504337a529c3d395cc54bc0a53cb9fc0eeb507b Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 11:02:23 +0700 Subject: [PATCH 05/10] update --- .../server/graphql/data/GraphQLField.java | 7 +++- .../graphql/test/data/GraphQLFieldTest.java | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java index 85f56f19..42173a0f 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLField.java @@ -105,7 +105,7 @@ public T getFieldArgumentValue( ) { GraphQLField field = this; for (String fieldName : fieldNames) { - field = fieldByName.get(fieldName); + field = field.getField(fieldName); if (field == null) { break; } @@ -149,6 +149,11 @@ public T getFieldArgumentValue( if (value == null) { return null; } + if (value instanceof String) { + return SingletonStringDeserializer + .getInstance() + .deserializeOrNull((String) value, type); + } return (T) EzySingletonOutputTransformer .getInstance() .transform(value, type); diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java index 274545cf..43608996 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java @@ -83,4 +83,44 @@ public void toStringTest() { .arguments(Collections.emptyMap()) ); } + + @Test + public void getNestedFieldArgumentValueTest() { + // given + GraphQLField field = GraphQLField.builder() + .name("user") + .addField( + GraphQLField.builder() + .name("bank") + .addField( + GraphQLField.builder() + .name("branch") + .arguments( + EzyMapBuilder.mapBuilder() + .put("id", "1") + .toMap() + ) + .build() + ) + .build() + ) + .build(); + + // when + String value = field.getFieldArgumentValue( + "id", + "bank", + "branch" + ); + Long typedValue = field.getFieldArgumentValue( + "id", + Long.class, + "bank", + "branch" + ); + + // then + Asserts.assertEquals(value, "1"); + Asserts.assertEquals(typedValue, 1L); + } } From e199e8a2c6bd231996afd20ab50bde05149dd5e3 Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 11:09:56 +0700 Subject: [PATCH 06/10] update --- .../graphql/scheme/GraphQLSchemaParser.java | 8 ++- .../test/scheme/GraphQLSchemaParserTest.java | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java index 819c0584..56eeed3f 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java @@ -213,6 +213,9 @@ private StringBuilder forwardStandardize(String query) { if (ch == '{' || ch == '}') { answer.append(ch); } else if (ch == '+' || ch == ',' || ch == ' ' || ch == '\t' || ch == '\n') { + if (answer.length() == 0) { + continue; + } char lastChar = answer.charAt(answer.length() - 1); if ((lastChar != ' ') && (lastChar != '{')) { answer.append(' '); @@ -232,6 +235,9 @@ private StringBuilder backwardStandardize(String query) { if (ch == '{' || ch == '}') { answer.insert(0, ch); } else if (ch == ' ') { // ',' '\t' '\n' '+' have been removed in forward pass + if (answer.length() == 0) { + continue; + } char firstChar = answer.charAt(0); if ((firstChar != '{') && (firstChar != '}')) { answer.insert(0, ' '); @@ -265,7 +271,7 @@ private int findOperationSelectionStart(String s, int start) { int quotesCount = 0; int length = s.length(); for (int i = start; i < length; ++i) { - char prevCh = s.charAt(i - 1); + char prevCh = i > 0 ? s.charAt(i - 1) : 0; char ch = s.charAt(i); if (prevCh != '\\' && ch == '\'') { quoteCount = quoteCount == 0 ? 1 : 0; diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java index bb1c68a8..d9f6c2ae 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java @@ -91,6 +91,42 @@ public void testStandardize3() { } } + @Test + public void standardizeQueryWithLeadingSeparatorTest() { + // given + GraphQLSchemaParser parser = new GraphQLSchemaParser( + new ObjectMapper() + ); + + // when + GraphQLSchema schema = parser.parseQuery( + ",{slug}", + Collections.emptyMap() + ); + + // then + Asserts.assertEquals(schema.getQueryDefinitions().size(), 1); + Asserts.assertEquals(schema.getQueryDefinitions().get(0).getName(), "slug"); + } + + @Test + public void standardizeQueryWithTrailingSeparatorTest() { + // given + GraphQLSchemaParser parser = new GraphQLSchemaParser( + new ObjectMapper() + ); + + // when + GraphQLSchema schema = parser.parseQuery( + "{slug},", + Collections.emptyMap() + ); + + // then + Asserts.assertEquals(schema.getQueryDefinitions().size(), 1); + Asserts.assertEquals(schema.getQueryDefinitions().get(0).getName(), "slug"); + } + @Test public void testStandardize4() { // given @@ -704,4 +740,28 @@ public void requireParentBuilderWithEmptyStackTest() { false ); } + + @Test + public void findOperationSelectionStartFromZeroTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "{slug}", + 0 + ); + + // then + Asserts.assertEquals(result, 0); + } } From 90bd1dc9cb1e73fac67e0fcfd1e22da026e56b4d Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 11:12:59 +0700 Subject: [PATCH 07/10] update --- .../graphql/data/GraphQLDataFilter.java | 45 ++++++++++++++----- .../test/data/GraphQLDataFilterTest.java | 38 ++++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java index 6c24c842..1caa6239 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLDataFilter.java @@ -35,7 +35,13 @@ public List filterList( ) { List answer = new LinkedList<>(); Deque stack = new ArrayDeque<>(); - pushListItems(stack, dataList, queryDefinition, answer); + pushListItems( + stack, + dataList, + queryDefinition, + answer, + queryDefinition.getName() + ); filterStack(stack); return answer; } @@ -76,14 +82,15 @@ private void filterStack(Deque stack) { } else if (value instanceof List) { List newList = new LinkedList<>(); entry.output.put(fieldName, newList); - pushListItems(stack, (List) value, field, newList); - } else { - throw new GraphQLInvalidSchemeException( - EzyMapBuilder.mapBuilder() - .put("schema", "invalid") - .put("field", fieldName) - .toMap() + pushListItems( + stack, + (List) value, + field, + newList, + fieldName ); + } else { + throw newInvalidSchemeException(fieldName); } } } @@ -92,18 +99,34 @@ private void filterStack(Deque stack) { @SuppressWarnings({"rawtypes", "unchecked"}) private void pushListItems( Deque stack, - List dataList, + List dataList, GraphQLField queryDefinition, - List answer + List answer, + String fieldName ) { int size = dataList.size(); for (int i = size - 1; i >= 0; --i) { + Object data = dataList.get(i); + if (!(data instanceof Map)) { + throw newInvalidSchemeException(fieldName); + } Map item = new HashMap<>(); answer.add(0, item); - stack.push(new StackEntry(queryDefinition, dataList.get(i), item)); + stack.push(new StackEntry(queryDefinition, (Map) data, item)); } } + private GraphQLInvalidSchemeException newInvalidSchemeException( + String fieldName + ) { + return new GraphQLInvalidSchemeException( + EzyMapBuilder.mapBuilder() + .put("schema", "invalid") + .put("field", fieldName) + .toMap() + ); + } + @AllArgsConstructor @SuppressWarnings("rawtypes") private static class StackEntry { diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java index 489f9945..d6173afd 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java @@ -2,6 +2,7 @@ import com.tvd12.ezyhttp.server.graphql.data.GraphQLDataFilter; import com.tvd12.ezyhttp.server.graphql.data.GraphQLField; +import com.tvd12.ezyhttp.server.graphql.exception.GraphQLInvalidSchemeException; import com.tvd12.test.assertion.Asserts; import org.testng.annotations.Test; @@ -175,4 +176,41 @@ public void filterListWithAllFieldsTest() { false ); } + + @Test + public void filterListFieldWithNonMapItemTest() { + // given + GraphQLField queryDefinition = GraphQLField.builder() + .name("user") + .addField( + GraphQLField.builder() + .name("tags") + .addField( + GraphQLField.builder() + .name("name") + .build() + ) + .build() + ) + .build(); + Map data = mapBuilder() + .put("tags", Arrays.asList("java", "graphql")) + .toMap(); + + // when + Throwable e = Asserts.assertThrows(() -> + instance.filter(data, queryDefinition) + ); + + // then + Asserts.assertEqualsType(e, GraphQLInvalidSchemeException.class); + Asserts.assertEquals( + ((GraphQLInvalidSchemeException) e).getErrors(), + mapBuilder() + .put("schema", "invalid") + .put("field", "tags") + .toMap(), + false + ); + } } From 38e76b2713e5a722cd001276acbe7e4376d1ecb2 Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 14:25:10 +0700 Subject: [PATCH 08/10] update --- .../test/data/GraphQLDataFilterTest.java | 19 + .../graphql/test/data/GraphQLFieldTest.java | 58 ++ .../test/scheme/GraphQLSchemaParserTest.java | 702 ++++++++++++++++++ 3 files changed, 779 insertions(+) diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java index d6173afd..131fad24 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLDataFilterTest.java @@ -67,6 +67,25 @@ public void filterWithAllFieldsTest() { ); } + @Test + public void filterWithAllFieldsAndNullDataTest() { + // given + GraphQLField queryDefinition = GraphQLField.builder() + .name("user") + .addField( + GraphQLField.builder() + .name(ALL_FIELDS) + .build() + ) + .build(); + + // when + Map result = instance.filter(null, queryDefinition); + + // then + Asserts.assertEquals(result, Collections.emptyMap(), false); + } + @Test public void filterWithAllFieldsAndNestedOverrideTest() { // given diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java index 43608996..321c0912 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/data/GraphQLFieldTest.java @@ -84,6 +84,30 @@ public void toStringTest() { ); } + @Test + public void toStringWithMultipleFieldsTest() { + // given + GraphQLField field = GraphQLField.builder() + .name("user") + .addField( + GraphQLField.builder() + .name("name") + .build() + ) + .addField( + GraphQLField.builder() + .name("email") + .build() + ) + .build(); + + // when + String result = field.toString(); + + // then + Asserts.assertEquals(result, "user, [name, [], email, []]"); + } + @Test public void getNestedFieldArgumentValueTest() { // given @@ -123,4 +147,38 @@ public void getNestedFieldArgumentValueTest() { Asserts.assertEquals(value, "1"); Asserts.assertEquals(typedValue, 1L); } + + @Test + public void getNestedFieldArgumentValueWithNonStringValueTest() { + // given + GraphQLField field = GraphQLField.builder() + .name("user") + .addField( + GraphQLField.builder() + .name("bank") + .addField( + GraphQLField.builder() + .name("branch") + .arguments( + EzyMapBuilder.mapBuilder() + .put("id", 1) + .toMap() + ) + .build() + ) + .build() + ) + .build(); + + // when + Long typedValue = field.getFieldArgumentValue( + "id", + Long.class, + "bank", + "branch" + ); + + // then + Asserts.assertEquals(typedValue, 1L); + } } diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java index d9f6c2ae..819dfba1 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java @@ -458,6 +458,313 @@ public void parseQueryWithVariablesInListAndNestedObject() { ); } + @Test + public void replaceVariablePlaceholdersWithNullArgumentsTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "replaceVariablePlaceholders", + Map.class, + Map.class + ); + method.setAccessible(true); + + // when + method.invoke( + instance, + null, + Collections.singletonMap("id", 1) + ); + + // then + Asserts.assertTrue(true); + } + + @Test + public void replaceVariablePlaceholdersWithNullVariablesTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "replaceVariablePlaceholders", + Map.class, + Map.class + ); + method.setAccessible(true); + Map arguments = EzyMapBuilder.mapBuilder() + .put("id", 1) + .toMap(); + + // when + method.invoke( + instance, + arguments, + null + ); + + // then + Asserts.assertEquals( + arguments, + EzyMapBuilder.mapBuilder() + .put("id", 1) + .toMap(), + false + ); + } + + @Test + public void replaceVariablePlaceholdersWithListValueTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "replaceVariablePlaceholders", + Map.class, + Map.class + ); + method.setAccessible(true); + Map arguments = EzyMapBuilder.mapBuilder() + .put( + "ids", + Arrays.asList( + EzyMapBuilder.mapBuilder() + .put("__ezyhttp_graphql_variable__", "id") + .toMap() + ) + ) + .toMap(); + + // when + method.invoke( + instance, + arguments, + Collections.singletonMap("id", 1) + ); + + // then + Asserts.assertEquals( + arguments, + EzyMapBuilder.mapBuilder() + .put("ids", Collections.singletonList(1)) + .toMap(), + false + ); + } + + @Test + public void replaceVariablePlaceholdersWithNestedListValueTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "replaceVariablePlaceholders", + Map.class, + Map.class + ); + method.setAccessible(true); + Map arguments = EzyMapBuilder.mapBuilder() + .put( + "filters", + Arrays.asList( + EzyMapBuilder.mapBuilder() + .put( + "names", + Arrays.asList( + EzyMapBuilder.mapBuilder() + .put("__ezyhttp_graphql_variable__", "name") + .toMap() + ) + ) + .toMap() + ) + ) + .toMap(); + + // when + method.invoke( + instance, + arguments, + Collections.singletonMap("name", "Dzung") + ); + + // then + Asserts.assertEquals( + arguments, + EzyMapBuilder.mapBuilder() + .put( + "filters", + Collections.singletonList( + EzyMapBuilder.mapBuilder() + .put( + "names", + Collections.singletonList("Dzung") + ) + .toMap() + ) + ) + .toMap(), + false + ); + } + + @Test + public void replaceVariablePlaceholdersShouldPushMapItemInListTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "replaceVariablePlaceholders", + Map.class, + Map.class + ); + method.setAccessible(true); + Map arguments = EzyMapBuilder.mapBuilder() + .put( + "filters", + Arrays.asList( + EzyMapBuilder.mapBuilder() + .put( + "name", + EzyMapBuilder.mapBuilder() + .put("__ezyhttp_graphql_variable__", "name") + .toMap() + ) + .toMap() + ) + ) + .toMap(); + + // when + method.invoke( + instance, + arguments, + Collections.singletonMap("name", "Dzung") + ); + + // then + Asserts.assertEquals( + arguments, + EzyMapBuilder.mapBuilder() + .put( + "filters", + Collections.singletonList( + EzyMapBuilder.mapBuilder() + .put("name", "Dzung") + .toMap() + ) + ) + .toMap(), + false + ); + } + + @Test + public void replaceVariablePlaceholdersShouldPushListItemInListTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "replaceVariablePlaceholders", + Map.class, + Map.class + ); + method.setAccessible(true); + Map arguments = EzyMapBuilder.mapBuilder() + .put( + "groups", + Arrays.asList( + Arrays.asList( + EzyMapBuilder.mapBuilder() + .put("__ezyhttp_graphql_variable__", "id") + .toMap() + ) + ) + ) + .toMap(); + + // when + method.invoke( + instance, + arguments, + Collections.singletonMap("id", 1) + ); + + // then + Asserts.assertEquals( + arguments, + EzyMapBuilder.mapBuilder() + .put( + "groups", + Collections.singletonList( + Collections.singletonList(1) + ) + ) + .toMap(), + false + ); + } + + @Test + public void getVariableValueWithStringVariableNameTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "getVariableValue", + Object.class, + Map.class + ); + method.setAccessible(true); + Map placeholder = EzyMapBuilder.mapBuilder() + .put("__ezyhttp_graphql_variable__", "id") + .toMap(); + + // when + Object result = method.invoke( + instance, + placeholder, + Collections.singletonMap("id", 1) + ); + + // then + Asserts.assertEquals(result, 1); + } + + @Test + public void getVariableValueWithNonStringVariableNameTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "getVariableValue", + Object.class, + Map.class + ); + method.setAccessible(true); + Map placeholder = EzyMapBuilder.mapBuilder() + .put("__ezyhttp_graphql_variable__", 1) + .toMap(); + + // when + Object result = method.invoke( + instance, + placeholder, + Collections.singletonMap("id", 1) + ); + + // then + Asserts.assertNull(result); + } + @Test public void parseQuerySubmitWorkReportWithOperationVariables() { // given @@ -764,4 +1071,399 @@ public void findOperationSelectionStartFromZeroTest() throws Exception { // then Asserts.assertEquals(result, 0); } + + @Test + public void findOperationSelectionStartFromLengthTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + String query = "query"; + + // when + Object result = method.invoke( + instance, + query, + query.length() + ); + + // then + Asserts.assertEquals(result, -1); + } + + @Test + public void findOperationSelectionStartShouldUseStartIndexTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "{ignored} query {slug}", + 10 + ); + + // then + Asserts.assertEquals(result, 16); + } + + @Test + public void findOperationSelectionStartShouldIgnoreBracesInSingleQuotesTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(arg: '{ignored}'){slug}", + 5 + ); + + // then + Asserts.assertEquals(result, 23); + } + + @Test + public void findOperationSelectionStartShouldIgnoreBracesInDoubleQuotesTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(arg: \"{ignored}\"){slug}", + 5 + ); + + // then + Asserts.assertEquals(result, 23); + } + + @Test + public void findOperationSelectionStartShouldIgnoreEscapedSingleQuoteTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(arg: 'it\\'s {ignored}'){slug}", + 5 + ); + + // then + Asserts.assertEquals(result, 29); + } + + @Test + public void findOperationSelectionStartShouldIgnoreEscapedDoubleQuoteTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(arg: \"he\\\"llo {ignored}\"){slug}", + 5 + ); + + // then + Asserts.assertEquals(result, 31); + } + + @Test + public void findOperationSelectionStartShouldReturnMinusOneInOpenSingleQuoteTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(arg: '{ignored}){slug}", + 5 + ); + + // then + Asserts.assertEquals(result, -1); + } + + @Test + public void findOperationSelectionStartShouldReturnMinusOneInOpenDoubleQuoteTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(arg: \"{ignored}){slug}", + 5 + ); + + // then + Asserts.assertEquals(result, -1); + } + + @Test + public void findOperationSelectionStartShouldIgnoreBracesInParenthesesTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(input: {id: 1}){slug}", + 5 + ); + + // then + Asserts.assertEquals(result, 21); + } + + @Test + public void findOperationSelectionStartShouldReturnMinusOneWhenBracesOnlyInParenthesesTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "findOperationSelectionStart", + String.class, + int.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query(input: {id: 1})", + 5 + ); + + // then + Asserts.assertEquals(result, -1); + } + + @Test + public void removeQueryPrefixWithExactPrefixTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "removeQueryPrefix", + String.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query" + ); + + // then + Asserts.assertEquals(result, ""); + } + + @Test + public void removeQueryPrefixWithNonNameCharAfterPrefixTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "removeQueryPrefix", + String.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query{slug}" + ); + + // then + Asserts.assertEquals(result, "{slug}"); + } + + @Test + public void removeQueryPrefixWithOperationNameAndSelectionStartTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "removeQueryPrefix", + String.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query FindSlug($id: ID){slug}" + ); + + // then + Asserts.assertEquals(result, "{slug}"); + } + + @Test + public void removeQueryPrefixWithoutSelectionStartAfterPrefixTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "removeQueryPrefix", + String.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query FindSlug" + ); + + // then + Asserts.assertEquals(result, " FindSlug"); + } + + @Test + public void removeQueryPrefixWithLetterAfterPrefixTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "removeQueryPrefix", + String.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "queryName{slug}" + ); + + // then + Asserts.assertEquals(result, "queryName{slug}"); + } + + @Test + public void removeQueryPrefixWithDigitAfterPrefixTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "removeQueryPrefix", + String.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query1{slug}" + ); + + // then + Asserts.assertEquals(result, "query1{slug}"); + } + + @Test + public void removeQueryPrefixWithUnderscoreAfterPrefixTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new ObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "removeQueryPrefix", + String.class + ); + method.setAccessible(true); + + // when + Object result = method.invoke( + instance, + "query_name{slug}" + ); + + // then + Asserts.assertEquals(result, "query_name{slug}"); + } } From c5021455f6f6c058405484315b82e165dc04b915 Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 14:34:09 +0700 Subject: [PATCH 09/10] update --- .../graphql/scheme/GraphQLSchemaParser.java | 5 ++- .../test/scheme/GraphQLSchemaParserTest.java | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java index 56eeed3f..008c3370 100644 --- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java +++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/scheme/GraphQLSchemaParser.java @@ -253,7 +253,8 @@ private String removeQueryPrefix(String s) { String prefix = "query"; if (s.startsWith(prefix) && (s.length() == prefix.length() - || !isGraphQLNameChar(s.charAt(prefix.length())))) { + || !isGraphQLNameChar(s.charAt(prefix.length()))) + ) { int selectionStart = findOperationSelectionStart( s, prefix.length() @@ -355,7 +356,7 @@ private void replaceVariablePlaceholders( stack.push(value); } } - } else if (item instanceof List) { + } else { List list = (List) item; int size = list.size(); for (int i = 0; i < size; ++i) { diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java index 819dfba1..9dc59f31 100644 --- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java +++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/scheme/GraphQLSchemaParserTest.java @@ -711,6 +711,45 @@ public void replaceVariablePlaceholdersShouldPushListItemInListTest() throws Exc ); } + @Test + public void replaceVariablePlaceholdersWithScalarListValueTest() throws Exception { + // given + GraphQLSchemaParser instance = new GraphQLSchemaParser( + new GraphQLObjectMapperFactory().newObjectMapper() + ); + Method method = GraphQLSchemaParser.class.getDeclaredMethod( + "replaceVariablePlaceholders", + Map.class, + Map.class + ); + method.setAccessible(true); + Map arguments = EzyMapBuilder.mapBuilder() + .put( + "tags", + Arrays.asList("java", 1, null) + ) + .toMap(); + + // when + method.invoke( + instance, + arguments, + Collections.singletonMap("id", 1) + ); + + // then + Asserts.assertEquals( + arguments, + EzyMapBuilder.mapBuilder() + .put( + "tags", + Arrays.asList("java", 1, null) + ) + .toMap(), + false + ); + } + @Test public void getVariableValueWithStringVariableNameTest() throws Exception { // given From f584c6ae61409acce0897f394c0cbefe355c377b Mon Sep 17 00:00:00 2001 From: tvd12 Date: Fri, 26 Jun 2026 14:35:05 +0700 Subject: [PATCH 10/10] version 1.5.4 --- ezyhttp-client/pom.xml | 2 +- ezyhttp-core/pom.xml | 2 +- ezyhttp-server-boot/pom.xml | 2 +- ezyhttp-server-core/pom.xml | 2 +- ezyhttp-server-graphql/pom.xml | 2 +- ezyhttp-server-jetty/pom.xml | 2 +- ezyhttp-server-management/pom.xml | 2 +- ezyhttp-server-thymeleaf/pom.xml | 2 +- ezyhttp-server-tomcat/pom.xml | 2 +- pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ezyhttp-client/pom.xml b/ezyhttp-client/pom.xml index 0aa02aa4..7d4fe77b 100644 --- a/ezyhttp-client/pom.xml +++ b/ezyhttp-client/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-client diff --git a/ezyhttp-core/pom.xml b/ezyhttp-core/pom.xml index c6659cb2..ebd46f58 100644 --- a/ezyhttp-core/pom.xml +++ b/ezyhttp-core/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-core diff --git a/ezyhttp-server-boot/pom.xml b/ezyhttp-server-boot/pom.xml index d9791c52..8829c28a 100644 --- a/ezyhttp-server-boot/pom.xml +++ b/ezyhttp-server-boot/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-server-boot diff --git a/ezyhttp-server-core/pom.xml b/ezyhttp-server-core/pom.xml index ef6f578a..cb8cf3dc 100644 --- a/ezyhttp-server-core/pom.xml +++ b/ezyhttp-server-core/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-server-core diff --git a/ezyhttp-server-graphql/pom.xml b/ezyhttp-server-graphql/pom.xml index a92e7d76..40e5a6ad 100644 --- a/ezyhttp-server-graphql/pom.xml +++ b/ezyhttp-server-graphql/pom.xml @@ -6,7 +6,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-server-graphql ezyhttp-server-graphql diff --git a/ezyhttp-server-jetty/pom.xml b/ezyhttp-server-jetty/pom.xml index 98be4033..9637ab5a 100644 --- a/ezyhttp-server-jetty/pom.xml +++ b/ezyhttp-server-jetty/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-server-jetty diff --git a/ezyhttp-server-management/pom.xml b/ezyhttp-server-management/pom.xml index 1ea72835..f6c0e4eb 100644 --- a/ezyhttp-server-management/pom.xml +++ b/ezyhttp-server-management/pom.xml @@ -6,7 +6,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-server-management ezyhttp-server-management diff --git a/ezyhttp-server-thymeleaf/pom.xml b/ezyhttp-server-thymeleaf/pom.xml index 9c5ea43c..21b1251a 100644 --- a/ezyhttp-server-thymeleaf/pom.xml +++ b/ezyhttp-server-thymeleaf/pom.xml @@ -6,7 +6,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-server-thymeleaf ezyhttp-server-thymeleaf diff --git a/ezyhttp-server-tomcat/pom.xml b/ezyhttp-server-tomcat/pom.xml index a6199bbf..1565073c 100644 --- a/ezyhttp-server-tomcat/pom.xml +++ b/ezyhttp-server-tomcat/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.5.3 + 1.5.4 ezyhttp-server-tomcat diff --git a/pom.xml b/pom.xml index 84c94995..3358de15 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ 1.0.7 ezyhttp - 1.5.3 + 1.5.4 pom ezyhttp