diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/controller/GraphQLController.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/controller/GraphQLController.java
index 5c560ea1..b1ac3efb 100644
--- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/controller/GraphQLController.java
+++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/controller/GraphQLController.java
@@ -62,9 +62,11 @@ public GraphQLController(Builder builder) {
* curl --location -g --request GET 'http://localhost:8083/graphql?query={me{myVariable: $someValue}{id+name+friends{name}}}&variables={"id" : 1}'
*
*
- * @param arguments the request arguments
- * @param query GraphQL query
- * @param variables a JSON-encoded string like { "myVariable": "someValue", ... }
+ * @param arguments the request arguments
+ * @param query GraphQL query
+ * @param operationName optional operation name for named operations
+ * @param variables a JSON-encoded string like
+ * { "myVariable": "someValue", ... }
* @return the result
*/
@SuppressWarnings("unchecked")
@@ -72,6 +74,7 @@ public GraphQLController(Builder builder) {
public Object doGet(
RequestArguments arguments,
@RequestParam("query") String query,
+ @RequestParam("operationName") String operationName,
@RequestParam("variables") String variables
) {
Map variableMap = Collections.emptyMap();
@@ -92,7 +95,7 @@ public Object doGet(
);
}
}
- return fetch(arguments, query, variableMap);
+ return fetch(arguments, query, operationName, variableMap);
}
/**
@@ -119,6 +122,7 @@ public Object doPost(
return fetch(
arguments,
request.getQuery(),
+ request.getOperationName(),
request.getVariables()
);
}
@@ -127,9 +131,11 @@ public Object doPost(
private Object fetch(
RequestArguments arguments,
String query,
+ String operationName,
Map variables
) {
- GraphQLSchema schema = schemaParser.parseQuery(query, variables);
+ GraphQLSchema schema = schemaParser
+ .parseQuery(query, operationName, variables);
List queryDefinitions = schema
.getQueryDefinitions();
Map answer = new HashMap<>();
diff --git a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLRequest.java b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLRequest.java
index c8869fa6..c7a7c526 100644
--- a/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLRequest.java
+++ b/ezyhttp-server-graphql/src/main/java/com/tvd12/ezyhttp/server/graphql/data/GraphQLRequest.java
@@ -10,6 +10,7 @@
@Setter
public class GraphQLRequest {
private String query;
+ private String operationName;
private Map variables;
public Map getVariables() {
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 6f3c62d1..7e83710d 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
@@ -14,6 +14,7 @@
import java.util.Map;
import static com.tvd12.ezyfox.io.EzyStrings.EMPTY_STRING;
+import static com.tvd12.ezyfox.io.EzyStrings.isNotBlank;
@AllArgsConstructor
public final class GraphQLSchemaParser {
@@ -23,6 +24,28 @@ public final class GraphQLSchemaParser {
private static final String VARIABLE_PLACEHOLDER_FIELD =
"__ezyhttp_graphql_variable__";
+ public GraphQLSchema parseQuery(
+ String queryToParse,
+ String operationName,
+ Map variables
+ ) {
+ if (!isNotBlank(operationName)) {
+ return parseQuery(queryToParse, variables);
+ }
+ String standardized = standardizeKeepOperationNames(queryToParse);
+ String selectionSet = extractNamedOperation(standardized, operationName);
+ if (selectionSet == null) {
+ throw new GraphQLObjectMapperException(
+ Collections.singletonList(
+ GraphQLError.builder()
+ .message("unknown operation named: " + operationName)
+ .build()
+ )
+ );
+ }
+ return parseQuery(selectionSet, variables);
+ }
+
@SuppressWarnings({"unchecked", "MethodLength"})
public GraphQLSchema parseQuery(
String queryToParse,
@@ -77,8 +100,6 @@ public GraphQLSchema parseQuery(
);
replaceVariablePlaceholders(argumentMap, variables);
childBuilder.arguments(argumentMap);
- } catch (GraphQLObjectMapperException e) {
- throw e;
} catch (Exception e) {
throw new GraphQLObjectMapperException(
Collections.singletonList(
@@ -176,6 +197,42 @@ public GraphQLSchema parseQuery(
return schemaBuilder.build();
}
+ private String extractNamedOperation(
+ String standardizedQuery,
+ String operationName
+ ) {
+ int nameIdx = standardizedQuery.indexOf(operationName);
+ while (nameIdx >= 0) {
+ boolean validPrefix = nameIdx == 0
+ || !isGraphQLNameChar(standardizedQuery.charAt(nameIdx - 1));
+ int afterName = nameIdx + operationName.length();
+ boolean validSuffix = afterName >= standardizedQuery.length()
+ || !isGraphQLNameChar(standardizedQuery.charAt(afterName));
+ if (validPrefix && validSuffix) {
+ int braceStart = afterName;
+ while (braceStart < standardizedQuery.length()
+ && standardizedQuery.charAt(braceStart) != '{') {
+ braceStart++;
+ }
+ if (braceStart < standardizedQuery.length()) {
+ int depth = 0;
+ for (int i = braceStart; i < standardizedQuery.length(); i++) {
+ char c = standardizedQuery.charAt(i);
+ if (c == '{') {
+ depth++;
+ } else if (c == '}') {
+ if (--depth == 0) {
+ return standardizedQuery.substring(braceStart, i + 1);
+ }
+ }
+ }
+ }
+ }
+ nameIdx = standardizedQuery.indexOf(operationName, nameIdx + 1);
+ }
+ return null;
+ }
+
/**
* Remove redundant '\t', '\n', '+', ',', ' ' in query.
*
@@ -186,14 +243,22 @@ private String standardize(String query) {
if (query == null) {
return EMPTY_STRING;
}
- String trimedQuery = query.trim();
- StringBuilder forwardStandard = forwardStandardize(trimedQuery);
+ String trimmedQuery = query.trim();
+ StringBuilder forwardStandard = forwardStandardize(trimmedQuery);
StringBuilder backwardStandard = backwardStandardize(
forwardStandard.toString()
);
return removeQueryPrefix(backwardStandard.toString());
}
+ private String standardizeKeepOperationNames(String query) {
+ if (query == null) {
+ return EMPTY_STRING;
+ }
+ StringBuilder forwardStandard = forwardStandardize(query.trim());
+ return backwardStandardize(forwardStandard.toString()).toString();
+ }
+
private StringBuilder forwardStandardize(String query) {
int queryLength = query.length();
StringBuilder answer = new StringBuilder();
diff --git a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/controller/GraphQLControllerTest.java b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/controller/GraphQLControllerTest.java
index 3451e9c1..628734ef 100644
--- a/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/controller/GraphQLControllerTest.java
+++ b/ezyhttp-server-graphql/src/test/java/com/tvd12/ezyhttp/server/graphql/test/controller/GraphQLControllerTest.java
@@ -76,7 +76,7 @@ public void test() throws Exception {
// when
Object meResult = controller.doPost(arguments, meRequest);
Throwable e = Asserts.assertThrows(() ->
- controller.doGet(arguments, heroQuery, null)
+ controller.doGet(arguments, heroQuery, null, null)
);
// then
@@ -234,7 +234,7 @@ public void getAllFriendFields() {
// when
Object meResult = controller.doPost(arguments, meRequest);
Throwable e = Asserts.assertThrows(() ->
- controller.doGet(arguments, heroQuery, null)
+ controller.doGet(arguments, heroQuery, null, null)
);
// then
@@ -290,7 +290,7 @@ public void testFetcherNotFoundException() {
String heroQuery = "{hero}";
// when
- Throwable e = Asserts.assertThrows(() -> controller.doGet(arguments, heroQuery, null));
+ Throwable e = Asserts.assertThrows(() -> controller.doGet(arguments, heroQuery, null, null));
// then
Asserts.assertEqualsType(e, GraphQLFetcherException.class);
@@ -338,7 +338,7 @@ public void testInterceptorFalse() {
String heroQuery = "{hero}";
// when
- Throwable e = Asserts.assertThrows(() -> controller.doGet(arguments, heroQuery, null));
+ Throwable e = Asserts.assertThrows(() -> controller.doGet(arguments, heroQuery, null, null));
// then
Asserts.assertEqualsType(e, GraphQLFetcherException.class);
@@ -413,7 +413,7 @@ public void testQueryWithVariables() {
// when
Throwable e = Asserts.assertThrows(() ->
- controller.doGet(arguments, welcomeQuery, variablesString)
+ controller.doGet(arguments, welcomeQuery, null, variablesString)
);
// then
@@ -478,8 +478,8 @@ public void testQueryWithNullVariableType() {
String fooQuery = "{foo{value(value:$value){*}}}";
// when
- Object fooResult1 = controller.doGet(arguments, fooQuery, "{\"value\": \"Bar\"}");
- Object fooResult2 = controller.doGet(arguments, fooQuery, null);
+ Object fooResult1 = controller.doGet(arguments, fooQuery, null, "{\"value\": \"Bar\"}");
+ Object fooResult2 = controller.doGet(arguments, fooQuery, null, null);
// then
Asserts.assertEquals(fooResult1.toString(), "{data={foo={value={bar=Bar}}}}");
@@ -609,7 +609,7 @@ public void doGetTestException() {
String heroQuery = "{hero}";
// when
- Throwable e = Asserts.assertThrows(() -> controller.doGet(arguments, heroQuery, "abc"));
+ Throwable e = Asserts.assertThrows(() -> controller.doGet(arguments, heroQuery, null, "abc"));
// then
Asserts.assertEqualsType(e, GraphQLObjectMapperException.class);
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 2c3bb854..46331386 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
@@ -7,6 +7,7 @@
import com.tvd12.ezyhttp.server.graphql.data.GraphQLField;
import com.tvd12.ezyhttp.server.graphql.exception.GraphQLObjectMapperException;
import com.tvd12.ezyhttp.server.graphql.json.GraphQLObjectMapperFactory;
+import com.tvd12.ezyhttp.server.graphql.query.GraphQLQueryDefinition;
import com.tvd12.ezyhttp.server.graphql.scheme.GraphQLSchema;
import com.tvd12.ezyhttp.server.graphql.scheme.GraphQLSchemaParser;
import com.tvd12.test.assertion.Asserts;
@@ -1495,4 +1496,208 @@ public void removeQueryPrefixWithUnderscoreAfterPrefixTest() throws Exception {
// then
Asserts.assertEquals(result, "query_name{slug}");
}
+
+ @Test
+ public void parseQueryWithOperationNameTest() {
+ // given
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetMe{me{name}} query GetHero{hero{id}}";
+
+ // when
+ GraphQLSchema schema = instance.parseQuery(
+ query,
+ "GetMe",
+ Collections.emptyMap()
+ );
+
+ // then
+ List definitions = schema.getQueryDefinitions();
+ Asserts.assertEquals(definitions.size(), 1);
+ Asserts.assertEquals(definitions.get(0).getName(), "me");
+ }
+
+ @Test
+ public void parseQueryWithOperationNameSelectSecondTest() {
+ // given
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetMe{me{name}} query GetHero{hero{id}}";
+
+ // when
+ GraphQLSchema schema = instance.parseQuery(
+ query,
+ "GetHero",
+ Collections.emptyMap()
+ );
+
+ // then
+ List definitions = schema.getQueryDefinitions();
+ Asserts.assertEquals(definitions.size(), 1);
+ Asserts.assertEquals(definitions.get(0).getName(), "hero");
+ }
+
+ @Test
+ public void parseQueryWithUnknownOperationNameTest() {
+ // given
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetMe{me{name}}";
+
+ // when
+ Throwable e = Asserts.assertThrows(() ->
+ instance.parseQuery(query, "GetUnknown", Collections.emptyMap())
+ );
+
+ // then
+ Asserts.assertEqualsType(e, GraphQLObjectMapperException.class);
+ List errors = ((GraphQLObjectMapperException) e).getErrors();
+ Asserts.assertEquals(errors.size(), 1);
+ Asserts.assertEquals(errors.get(0).getMessage(), "unknown operation named: GetUnknown");
+ }
+
+ @Test
+ public void parseQueryWithNullOperationNameTest() {
+ // given
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetMe{me{name}}";
+
+ // when — null operationName falls back to normal parsing
+ GraphQLSchema schema = instance.parseQuery(
+ query,
+ null,
+ Collections.emptyMap()
+ );
+
+ // then
+ List definitions = schema.getQueryDefinitions();
+ Asserts.assertEquals(definitions.size(), 1);
+ Asserts.assertEquals(definitions.get(0).getName(), "me");
+ }
+
+ @Test
+ public void parseQueryOperationNameSuffixOfAnotherShouldNotMatchTest() {
+ // given — "GetMe" appears as suffix inside "SomeGetMe"; should skip it
+ // and match the standalone "GetMe"
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query SomeGetMe{me{name}} query GetMe{user{id}}";
+
+ // when
+ GraphQLSchema schema = instance.parseQuery(
+ query,
+ "GetMe",
+ Collections.emptyMap()
+ );
+
+ // then — matched the second operation, not the one ending in "GetMe"
+ List definitions = schema.getQueryDefinitions();
+ Asserts.assertEquals(definitions.size(), 1);
+ Asserts.assertEquals(definitions.get(0).getName(), "user");
+ }
+
+ @Test
+ public void parseQueryOperationNameAtStartOfStringTest() {
+ // given — no "query" keyword prefix; nameIdx == 0
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "GetMe{me{name}}";
+
+ // when
+ GraphQLSchema schema = instance.parseQuery(
+ query,
+ "GetMe",
+ Collections.emptyMap()
+ );
+
+ // then
+ List definitions = schema.getQueryDefinitions();
+ Asserts.assertEquals(definitions.size(), 1);
+ Asserts.assertEquals(definitions.get(0).getName(), "me");
+ }
+
+ @Test
+ public void parseQueryOperationNamePrefixOfAnotherShouldNotMatchTest() {
+ // given — "GetMe" is a prefix of "GetMeFull"; validSuffix check must reject it
+ // and match the standalone "GetMe" that comes after
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetMeFull{me{name}} query GetMe{user{id}}";
+
+ // when
+ GraphQLSchema schema = instance.parseQuery(
+ query,
+ "GetMe",
+ Collections.emptyMap()
+ );
+
+ // then — matched the standalone "GetMe", not "GetMeFull"
+ List definitions = schema.getQueryDefinitions();
+ Asserts.assertEquals(definitions.size(), 1);
+ Asserts.assertEquals(definitions.get(0).getName(), "user");
+ }
+
+ @Test
+ public void parseQueryOperationNameWithCharsBetweenNameAndBraceTest() {
+ // given — "(id: 1)" sits between operation name and '{'; braceStart++ runs
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetMe(id: 1){me{name}}";
+
+ // when
+ GraphQLSchema schema = instance.parseQuery(
+ query,
+ "GetMe",
+ Collections.emptyMap()
+ );
+
+ // then
+ List definitions = schema.getQueryDefinitions();
+ Asserts.assertEquals(definitions.size(), 1);
+ Asserts.assertEquals(definitions.get(0).getName(), "me");
+ }
+
+ @Test
+ public void parseQueryOperationNameWithUnclosedBracesTest() {
+ // given — unclosed braces: for loop exhausts without depth reaching 0
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetMe{me{name";
+
+ // when
+ Throwable e = Asserts.assertThrows(() ->
+ instance.parseQuery(query, "GetMe", Collections.emptyMap())
+ );
+
+ // then
+ Asserts.assertEqualsType(e, GraphQLObjectMapperException.class);
+ List errors = ((GraphQLObjectMapperException) e).getErrors();
+ Asserts.assertEquals(errors.get(0).getMessage(), "unknown operation named: GetMe");
+ }
+
+ @Test
+ public void parseQueryOperationNameAtEndWithoutSelectionSetTest() {
+ // given — "GetMe" at end of string: afterName >= length → validSuffix=true
+ // but braceStart loop finds no '{' → returns null → unknown operation
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+ String query = "query GetOther{other{id}} query GetMe";
+
+ // when
+ Throwable e = Asserts.assertThrows(() ->
+ instance.parseQuery(query, "GetMe", Collections.emptyMap())
+ );
+
+ // then
+ Asserts.assertEqualsType(e, GraphQLObjectMapperException.class);
+ List errors = ((GraphQLObjectMapperException) e).getErrors();
+ Asserts.assertEquals(errors.get(0).getMessage(), "unknown operation named: GetMe");
+ }
+
+ @Test
+ public void parseQueryWithOperationNameAndNullQueryTest() {
+ // given — null query with operationName; standardizeKeepOperationNames returns ""
+ GraphQLSchemaParser instance = new GraphQLSchemaParser(new ObjectMapper());
+
+ // when
+ Throwable e = Asserts.assertThrows(() ->
+ instance.parseQuery(null, "GetMe", Collections.emptyMap())
+ );
+
+ // then — "GetMe" not found in empty string
+ Asserts.assertEqualsType(e, GraphQLObjectMapperException.class);
+ List errors = ((GraphQLObjectMapperException) e).getErrors();
+ Asserts.assertEquals(errors.get(0).getMessage(), "unknown operation named: GetMe");
+ }
}