From 8cc0e17103c7fea80b895ba3410229f2854e8a53 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 28 Mar 2023 08:16:36 -0400 Subject: [PATCH] DEVEXP-357 Can now submit GraphQL queries --- .../marklogic/client/impl/RowManagerImpl.java | 32 +++++++- .../com/marklogic/client/row/RowManager.java | 80 ++++++++++++------- .../client/test/rows/GraphQLTest.java | 76 ++++++++++++++++++ 3 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 marklogic-client-api/src/test/java/com/marklogic/client/test/rows/GraphQLTest.java diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/RowManagerImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/RowManagerImpl.java index ea08685b4..d16e6b0ff 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/RowManagerImpl.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/RowManagerImpl.java @@ -75,7 +75,7 @@ void setHandleRegistry(HandleFactoryRegistry handleRegistry) { this.handleRegistry = handleRegistry; } - @Override + @Override public PlanBuilder newPlanBuilder() { PlanBuilderImpl planBuilder = new PlanBuilderSubImpl(); @@ -211,7 +211,7 @@ public RowSet resultRows(Plan plan, Transaction transaction) { public void execute(Plan plan) { execute(plan, null); } - + @Override public void execute(Plan plan, Transaction transaction) { PlanBuilderBaseImpl.RequestPlan requestPlan = checkPlan(plan); @@ -268,7 +268,7 @@ public RowSet resultRowsAs(Plan plan, Class as, Transaction transactio .withColumnTypes(datatypeStyle) .withOutput(rowStructureStyle) .getRequestParameters(); - + RESTServiceResultIterator iter = submitPlan(requestPlan, params, transaction); RowSetObject rowset = new RowSetObject<>(rowFormat, datatypeStyle, rowStructureStyle, iter, rowHandle); rowset.init(); @@ -356,7 +356,31 @@ public T columnInfoAs(PlanBuilder.PreparePlan plan, Class as) { return handle.get(); } - private String getRowFormat(T rowHandle) { + @Override + public T graphql(JSONWriteHandle query, T resultsHandle) { + if (resultsHandle == null) { + throw new IllegalArgumentException("Must specify a handle for the results of the GraphQL query"); + } + RequestParameters params = new RequestParameters(); + // Must force the MIME type before passing this to OkHttpServices - which does the exact same check below, + // requiring that the query handle be an instance of HandleImplementation. + HandleAccessor.checkHandle(query, "write").setMimetype("application/graphql"); + return services.postResource(requestLogger, "rows/graphql", null, params, query, resultsHandle); + } + + @Override + public T graphqlAs(JSONWriteHandle query, Class as) { + ContentHandle handle = handleFor(as); + if (!(handle instanceof JSONReadHandle)) { + throw new IllegalArgumentException("The handle is not an instance of JSONReadHandle."); + } + if (graphql(query, (JSONReadHandle) handle) == null) { + return null; + } + return handle.get(); + } + + private String getRowFormat(T rowHandle) { if (rowHandle == null) { throw new IllegalArgumentException("Must specify a handle to iterate over the rows"); } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/row/RowManager.java b/marklogic-client-api/src/main/java/com/marklogic/client/row/RowManager.java index 12319057c..dffc7ab20 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/row/RowManager.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/row/RowManager.java @@ -53,18 +53,18 @@ public enum RowStructure{ARRAY, OBJECT} /** * Specifies whether to emit the data type of each column in each row or only in the header * in the response for requests made with the row manager. - * + * * The distinction is significant when getting the rows as JSON or XML. * Because the server supports columns with variant data types, the default is in each row. * You can configure the row manager to return more concise objects if the data type * is consistent or if you aren't using the data type. - * + * * @param style the part of the rowset that should contain data types */ void setDatatypeStyle(RowSetPart style); /** - * Returns whether each row should have an array or object structure + * Returns whether each row should have an array or object structure * in the response for requests made with the row manager. * @return the style of row structure */ @@ -72,10 +72,10 @@ public enum RowStructure{ARRAY, OBJECT} /** * Specifies whether to get each row as an object (the default) or as an array * in the response for requests made with the row manager. - * + * * The distinction is significant when getting the rows as JSON * and also when executing a map or reduce function on the server. - * + * * @param style the structure of rows in the response */ void setRowStructureStyle(RowStructure style); @@ -130,14 +130,14 @@ public enum RowStructure{ARRAY, OBJECT} /** * Execute the given plan without returning any result. - * + * * @param plan the definition of a plan */ void execute(Plan plan); /** * Execute the given plan without returning any result. - * + * * @param plan the definition of a plan * @param transaction a open transaction for the execute operation to run within */ @@ -170,7 +170,7 @@ public enum RowStructure{ARRAY, OBJECT} RowSet resultRows(Plan plan, T rowHandle); /** * Constructs and retrieves a set of database rows based on a plan using - * a JSON or XML handle for each row and reflecting documents written or + * a JSON or XML handle for each row and reflecting documents written or * deleted by an uncommitted transaction. * @param plan the definition of a plan for the database rows * @param rowHandle the JSON or XML handle that provides each row @@ -182,15 +182,15 @@ public enum RowStructure{ARRAY, OBJECT} /** * Constructs and retrieves a set of database rows based on a plan using - * a JSON or XML handle for each row and reflecting documents written or + * a JSON or XML handle for each row and reflecting documents written or * deleted by an uncommitted transaction. - * + * * The IO class must have been registered before creating the database client. - * By default, the provided handles that implement + * By default, the provided handles that implement * {@link com.marklogic.client.io.marker.ContentHandle ContentHandle} are registered. - * + * * Learn more about shortcut methods - * + * * @param plan the definition of a plan for the database rows * @param as the IO class for reading each row as JSON or XML content * @param the type of object that will be returned by the handle registered for it @@ -199,15 +199,15 @@ public enum RowStructure{ARRAY, OBJECT} RowSet resultRowsAs(Plan plan, Class as); /** * Constructs and retrieves a set of database rows based on a plan using - * a JSON or XML handle for each row and reflecting documents written or + * a JSON or XML handle for each row and reflecting documents written or * deleted by an uncommitted transaction. - * + * * The IO class must have been registered before creating the database client. - * By default, the provided handles that implement + * By default, the provided handles that implement * {@link com.marklogic.client.io.marker.ContentHandle ContentHandle} are registered. - * + * * Learn more about shortcut methods - * + * * @param plan the definition of a plan for the database rows * @param as the IO class for reading each row as JSON or XML content * @param transaction a open transaction for documents from which rows have been projected @@ -240,13 +240,13 @@ public enum RowStructure{ARRAY, OBJECT} /** * Constructs and retrieves a set of database rows based on a plan * in the representation specified by the IO class. - * + * * The IO class must have been registered before creating the database client. - * By default, the provided handles that implement + * By default, the provided handles that implement * {@link com.marklogic.client.io.marker.ContentHandle ContentHandle} are registered. - * + * * Learn more about shortcut methods - * + * * @param plan the definition of a plan for the database rows * @param as the IO class for reading the set of rows * @param the type of the IO object for reading the set of rows @@ -257,13 +257,13 @@ public enum RowStructure{ARRAY, OBJECT} * Constructs and retrieves a set of database rows based on a plan * in the representation specified by the IO class and reflecting * documents written or deleted by an uncommitted transaction. - * + * * The IO class must have been registered before creating the database client. - * By default, the provided handles that implement + * By default, the provided handles that implement * {@link com.marklogic.client.io.marker.ContentHandle ContentHandle} are registered. - * + * * Learn more about shortcut methods - * + * * @param plan the definition of a plan for the database rows * @param as the IO class for reading the set of rows * @param transaction a open transaction for documents from which rows have been projected @@ -284,13 +284,13 @@ public enum RowStructure{ARRAY, OBJECT} /** * Constructs a plan for retrieving a set of database rows and returns an explanation * of the plan in the representation specified by the IO class. - * + * * The IO class must have been registered before creating the database client. - * By default, the provided handles that implement + * By default, the provided handles that implement * {@link com.marklogic.client.io.marker.ContentHandle ContentHandle} are registered. - * + * * Learn more about shortcut methods - * + * * @param plan the definition of a plan for database rows * @param as the IO class for reading the explanation for the plan * @param the type of the IO object for reading the explanation @@ -361,4 +361,24 @@ public enum RowStructure{ARRAY, OBJECT} * @return an object of the IO class with the content of the column information for the plan */ T columnInfoAs(PlanBuilder.PreparePlan plan, Class as); + + /** + * Executes a GraphQL query against the database and returns the results as a JSON object. + * + * @param query the GraphQL query to execute + * @param resultsHandle the IO class for capturing the results + * @param the type of the IO object for r the results + * @return an object of the IO class containing the query results, which will include error messages if the query fails + */ + T graphql(JSONWriteHandle query, T resultsHandle); + + /** + * Executes a GraphQL query against the database and returns the results as a JSON object. + * + * @param query the GraphQL query to execute + * @param as the class type of the results to return; typically JsonNode or String + * @param the type of the results to return + * @return an instance of the given return type that contains the query results, which will include error messages if the query fails + */ + T graphqlAs(JSONWriteHandle query, Class as); } diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/GraphQLTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/GraphQLTest.java new file mode 100644 index 000000000..3a949e161 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/GraphQLTest.java @@ -0,0 +1,76 @@ +package com.marklogic.client.test.rows; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.marklogic.client.io.JacksonHandle; +import com.marklogic.client.io.StringHandle; +import com.marklogic.client.row.RowManager; +import com.marklogic.client.test.Common; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GraphQLTest extends AbstractOpticUpdateTest { + + private final static String QUERY = "query myQuery { opticUnitTest_musician {firstName lastName}}"; + + private RowManager rowManager; + + @BeforeEach + void beforeEach() { + rowManager = Common.newClient().newRowManager(); + } + + @Test + void jsonQuery() { + ObjectNode query = mapper.createObjectNode().put("query", QUERY); + JsonNode response = rowManager.graphql(new JacksonHandle(query), new JacksonHandle()).get(); + verifyResponse(response); + } + + @Test + void stringQuery() { + JsonNode response = rowManager.graphql(new StringHandle("{\"query\": \"" + QUERY + "\"}"), new JacksonHandle()).get(); + verifyResponse(response); + } + + @Test + void getResultAsJson() { + ObjectNode query = mapper.createObjectNode().put("query", QUERY); + JsonNode response = rowManager.graphqlAs(new JacksonHandle(query), JsonNode.class); + verifyResponse(response); + } + + @Test + void getResultAsString() throws Exception { + ObjectNode query = mapper.createObjectNode().put("query", QUERY); + String response = rowManager.graphqlAs(new JacksonHandle(query), String.class); + verifyResponse(mapper.readTree(response)); + } + + @Test + void invalidQuery() { + ObjectNode query = mapper.createObjectNode().put("query", "this is not valid"); + JsonNode response = rowManager.graphql(new JacksonHandle(query), new JacksonHandle()).get(); + + assertTrue(response.has("errors")); + assertEquals(1, response.get("errors").size()); + + String message = response.get("errors").get(0).get("message").asText(); + assertTrue(message.startsWith("GRAPHQL-PARSE: Error parsing the GraphQL request string"), + "Unexpected error message: " + message); + } + + private void verifyResponse(JsonNode response) { + JsonNode data = response.get("data"); + JsonNode musicians = data.get("opticUnitTest_musician"); + assertEquals(4, musicians.size()); + musicians.forEach(musician -> { + assertEquals(2, musician.size()); + assertTrue(musician.has("firstName")); + assertTrue(musician.has("lastName")); + }); + } +}