This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch feature/SLING-9655
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git

commit 4181c570e6c3d95efab1fa3dbc56a79b1aa9ba2e
Author: Radu Cotescu <[email protected]>
AuthorDate: Mon Aug 31 11:46:32 2020 +0200

    SLING-9655 - Caching support for the GraphQL core
    
    * made the suffix where the persisted queries HTTP API is enabled 
configurable; an empty suffix disables the API
    * documented the persisted queries HTTP API
---
 README.md                                          | 94 +++++++++++++++++++++
 pom.xml                                            |  6 +-
 .../sling/graphql/core/servlet/GraphQLServlet.java | 95 +++++++++++-----------
 .../graphql/core/servlet/GraphQLServletTest.java   | 81 ++++++++++++++++++
 4 files changed, 227 insertions(+), 49 deletions(-)

diff --git a/README.md b/README.md
index 812b912..8137f2a 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,100 @@ This module enables the following GraphQL "styles"
     
 The GraphQL requests hit a Sling resource in all cases, there's no need for 
path-mounted servlets which are [not 
desirable](https://sling.apache.org/documentation/the-sling-engine/servlets.html#caveats-when-binding-servlets-by-path-1).
 
+### Persisted queries API
+No matter how you decide to create your Sling GraphQL endpoints, you have the 
option to allow GraphQL clients to use persisted queries.
+Since most user agents have a character limit for GET requests and POST 
requests are not cacheable, a persisted query allows the best of
+both worlds.
+
+#### How does it work?
+1. An instance of the GraphQL servlet has to be configured; by default, the 
servlet will enable the persisted queries API on the
+ `/persisted` request suffix; the value is configurable, via the 
`persistedQueries.suffix` parameter of the factory configuration.
+2. A client prepares a persisted query in advance by `POST`ing the query text 
to the endpoint where the GraphQL servlet is bound, plus the
+ `/persisted` suffix.
+3. The servlet will respond with a `201 Created` status; the response's 
`Location` header will then instruct the client where it can then
+ execute the persisted query, via a `GET` request.
+4. The responses for a `GET` requests to a persisted query will contain 
appropriate HTTP Cache headers, allowing front-end HTTP caches
+ (e.g. CDNs) to cache the JSON responses. 
+5. There's no guarantee on how long a persisted query is stored. A client that 
gets a `404` on a persisted query must be prepared to
+ re`POST` the query, in order to store the prepared query again.
+    
+Here are a few examples:
+
+1. Storing a query
+    ```bash
+    curl -v 'http://localhost:8080/graphql.json/persisted' \
+      -H 'Content-Type: application/json' \
+      --data-binary '{"query":"{\n  navigation {\n    search\n    sections {\n 
     path\n      name\n    }\n  }\n  article(withText: \"virtual\") {\n    
path\n    title\n    seeAlso {\n      path\n      title\n      tags\n    }\n  
}\n}\n","variables":null}' \
+      --compressed
+    > POST /graphql.json/persisted HTTP/1.1
+    > Host: localhost:8080
+    > User-Agent: curl/7.64.1
+    > Accept: */*
+    > Accept-Encoding: deflate, gzip
+    > Content-Type: application/json
+    > Content-Length: 236
+    >
+    * upload completely sent off: 236 out of 236 bytes
+    < HTTP/1.1 201 Created
+    < Date: Mon, 31 Aug 2020 16:33:48 GMT
+    < X-Content-Type-Options: nosniff
+    < X-Frame-Options: SAMEORIGIN
+    < Location: 
http://localhost:8080/graphql.json/persisted/e1ce2e205e1dfb3969627c6f417860cadab696e0e87b1c44de1438848661b62f
+    < Content-Length: 0
+    ```
+2. Running a persisted query
+```bash
+curl -v 
http://localhost:8080/graphql.json/persisted/e1ce2e205e1dfb3969627c6f417860cadab696e0e87b1c44de1438848661b62f
+> GET 
/graphql.json/persisted/e1ce2e205e1dfb3969627c6f417860cadab696e0e87b1c44de1438848661b62f
 HTTP/1.1
+> Host: localhost:8080
+> User-Agent: curl/7.64.1
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< Date: Mon, 31 Aug 2020 16:35:18 GMT
+< X-Content-Type-Options: nosniff
+< X-Frame-Options: SAMEORIGIN
+< Cache-Control: max-age=60
+< Content-Type: application/json;charset=utf-8
+< Transfer-Encoding: chunked
+<
+
+{
+  "data": {
+    "navigation": {
+      "search": "/content/search",
+      "sections": [
+        {
+          "path": "/content/articles/travel",
+          "name": "Travel"
+        },
+        {
+          "path": "/content/articles/music",
+          "name": "Music"
+        }
+      ]
+    }
+    "article": [
+      {
+        "path": 
"/content/articles/travel/precious-kunze-on-the-bandwidth-of-virtual-nobis-id-aka-usb",
+        "title": "Travel - Precious Kunze on the bandwidth of virtual 'nobis 
id' (aka USB)",
+        "seeAlso": [
+          {
+            "path": 
"/content/articles/travel/solon-davis-on-the-card-of-primary-reiciendis-omnis-aka-sql",
+            "title": "Travel - Solon Davis on the card of primary 'reiciendis 
omnis' (aka SQL)",
+            "tags": [
+              "bandwidth",
+              "protocol"
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+
 ## Resource-specific GraphQL schemas
 
 Schemas are provided by `SchemaProvider` services:
diff --git a/pom.xml b/pom.xml
index 3c1a881..9c52ff7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -165,7 +165,7 @@
     <dependency>
       <groupId>org.apache.sling</groupId>
       <artifactId>org.apache.sling.scripting.api</artifactId>
-      <version>2.1.0</version>
+      <version>2.1.12</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
@@ -228,8 +228,8 @@
     </dependency>
     <dependency>
       <groupId>org.apache.sling</groupId>
-      <artifactId>org.apache.sling.testing.osgi-mock.junit4</artifactId>
-      <version>2.4.16</version>
+      <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+      <version>2.4.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git 
a/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java 
b/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
index 537875e..d346468 100644
--- a/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
+++ b/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
@@ -44,9 +44,6 @@ import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.ConfigurationPolicy;
 import org.osgi.service.component.annotations.Reference;
-import org.osgi.service.component.annotations.ReferenceCardinality;
-import org.osgi.service.component.annotations.ReferencePolicy;
-import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.AttributeType;
 import org.osgi.service.metatype.annotations.Designate;
@@ -79,9 +76,6 @@ public class GraphQLServlet extends SlingAllMethodsServlet {
 
     public static final String P_QUERY = "query";
 
-    private static final String SUFFIX_PERSISTED = "/persisted";
-    private static final Pattern PATTERN_GET_PERSISTED_QUERY = 
Pattern.compile("^" + SUFFIX_PERSISTED + "/([a-f0-9]{64})$");
-
     @ObjectClassDefinition(
         name = "Apache Sling GraphQL Servlet",
         description = "Servlet that implements GraphQL endpoints")
@@ -107,6 +101,12 @@ public class GraphQLServlet extends SlingAllMethodsServlet 
{
         String[] sling_servlet_extensions() default "gql";
 
         @AttributeDefinition(
+                name = "Persisted queries suffix",
+                description = "The request suffix under which the HTTP API for 
persisted queries should be made available."
+        )
+        String persistedQueries_suffix() default "/persisted";
+
+        @AttributeDefinition(
                 name = "Persisted Queries Cache-Control max-age",
                 description = "The maximum amount of time a persisted query 
resource is considered fresh (in seconds). A negative value " +
                         "will be interpreted as 0.",
@@ -125,52 +125,55 @@ public class GraphQLServlet extends 
SlingAllMethodsServlet {
     @Reference
     private SlingScalarsProvider scalarsProvider;
 
-    @Reference(
-            cardinality = ReferenceCardinality.OPTIONAL,
-            policy = ReferencePolicy.STATIC,
-            policyOption = ReferencePolicyOption.GREEDY
-    )
+    @Reference
     private GraphQLCacheProvider cacheProvider;
 
-    private final Config config;
-    private final int cacheControlMaxAge;
-
+    private String suffixPersisted;
+    private Pattern patternGetPersistedQuery;
+    private int cacheControlMaxAge;
     private final JsonSerializer jsonSerializer = new JsonSerializer();
 
     @Activate
-    public GraphQLServlet(Config config) {
-        this.config = config;
+    private void activate(Config config) {
         cacheControlMaxAge = config.cache$_$control_max$_$age() >= 0 ? 
config.cache$_$control_max$_$age() : 0;
+        String suffix = config.persistedQueries_suffix();
+        if (StringUtils.isNotEmpty(suffix) && suffix.startsWith("/")) {
+            suffixPersisted = suffix;
+            patternGetPersistedQuery = Pattern.compile("^" + suffixPersisted + 
"/([a-f0-9]{64})$");
+        } else {
+            suffixPersisted = null;
+            patternGetPersistedQuery = null;
+        }
     }
 
     @Override
     public void doGet(SlingHttpServletRequest request, 
SlingHttpServletResponse response) throws IOException {
         String suffix = request.getRequestPathInfo().getSuffix();
-        if (suffix != null && suffix.startsWith(SUFFIX_PERSISTED)) {
-            if (cacheProvider == null) {
-                
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "This servlet 
does not support persisted queries.");
-                return;
-            }
-            Matcher matcher = PATTERN_GET_PERSISTED_QUERY.matcher(suffix);
-            if (matcher.matches()) {
-                String queryHash = matcher.group(1);
-                if (StringUtils.isNotEmpty(queryHash)) {
-                    String query = cacheProvider.getQuery(queryHash, 
request.getResource().getResourceType(),
-                            request.getRequestPathInfo().getSelectorString());
-                    if (query != null) {
-                        boolean isAuthenticated = 
request.getHeaders("Authorization").hasMoreElements();
-                        StringBuilder cacheControlValue = new 
StringBuilder("max-age=").append(cacheControlMaxAge);
-                        if (isAuthenticated) {
-                            cacheControlValue.append(",private");
+        if (suffix != null) {
+            if (StringUtils.isNotEmpty(suffixPersisted) && 
suffix.startsWith(suffixPersisted)) {
+                Matcher matcher = patternGetPersistedQuery.matcher(suffix);
+                if (matcher.matches()) {
+                    String queryHash = matcher.group(1);
+                    if (StringUtils.isNotEmpty(queryHash)) {
+                        String query = cacheProvider.getQuery(queryHash, 
request.getResource().getResourceType(),
+                                
request.getRequestPathInfo().getSelectorString());
+                        if (query != null) {
+                            boolean isAuthenticated = 
request.getHeaders("Authorization").hasMoreElements();
+                            StringBuilder cacheControlValue = new 
StringBuilder("max-age=").append(cacheControlMaxAge);
+                            if (isAuthenticated) {
+                                cacheControlValue.append(",private");
+                            }
+                            response.addHeader("Cache-Control", 
cacheControlValue.toString());
+                            execute(query, request, response);
+                        } else {
+                            
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot find persisted 
query " + queryHash);
                         }
-                        response.addHeader("Cache-Control", 
cacheControlValue.toString());
-                        execute(query, request, response);
-                    } else {
-                        response.sendError(HttpServletResponse.SC_NOT_FOUND, 
"Cannot find persisted query " + queryHash);
                     }
+                } else {
+                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, 
"Unexpected hash.");
                 }
             } else {
-                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+                response.sendError(HttpServletResponse.SC_BAD_REQUEST, 
"Persisted queries are disabled.");
             }
         } else {
             execute(request.getResource(), request, response);
@@ -180,16 +183,16 @@ public class GraphQLServlet extends 
SlingAllMethodsServlet {
     @Override
     public void doPost(SlingHttpServletRequest request, 
SlingHttpServletResponse response) throws IOException {
         String suffix = request.getRequestPathInfo().getSuffix();
-        if (suffix != null && suffix.equals(SUFFIX_PERSISTED)) {
-            if (cacheProvider == null) {
-                
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "This servlet 
does not support persisted queries.");
-                return;
+        if (suffix != null) {
+            if (StringUtils.isNotEmpty(suffixPersisted) && 
suffix.equals(suffixPersisted)) {
+                String query = IOUtils.toString(request.getReader());
+                String hash = cacheProvider.cacheQuery(query, 
request.getResource().getResourceType(),
+                        request.getRequestPathInfo().getSelectorString());
+                response.addHeader("Location", getLocationHeaderValue(request, 
hash));
+                response.setStatus(HttpServletResponse.SC_CREATED);
+            } else {
+                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
             }
-            String query = IOUtils.toString(request.getReader());
-            String hash = cacheProvider.cacheQuery(query, 
request.getResource().getResourceType(),
-                    request.getRequestPathInfo().getSelectorString());
-            response.addHeader("Location", getLocationHeaderValue(request, 
hash));
-            response.setStatus(HttpServletResponse.SC_CREATED);
         } else {
             execute(request.getResource(), request, response);
         }
diff --git 
a/src/test/java/org/apache/sling/graphql/core/servlet/GraphQLServletTest.java 
b/src/test/java/org/apache/sling/graphql/core/servlet/GraphQLServletTest.java
new file mode 100644
index 0000000..da9dbab
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/graphql/core/servlet/GraphQLServletTest.java
@@ -0,0 +1,81 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.core.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.Servlet;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.servlets.ServletResolverConstants;
+import org.apache.sling.graphql.core.cache.SimpleGraphQLCacheProvider;
+import org.apache.sling.graphql.core.engine.SlingDataFetcherSelector;
+import org.apache.sling.graphql.core.scalars.SlingScalarsProvider;
+import org.apache.sling.graphql.core.schema.RankedSchemaProviders;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockRequestPathInfo;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import 
org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+public class GraphQLServletTest {
+
+    @Rule
+    public SlingContext context = new SlingContext();
+
+    @Test
+    public void testDisabledSuffix() throws IOException {
+        RankedSchemaProviders rankedSchemaProviders = 
mock(RankedSchemaProviders.class);
+        context.registerService(rankedSchemaProviders);
+        SlingDataFetcherSelector slingDataFetcherSelector = 
mock(SlingDataFetcherSelector.class);
+        context.registerService(slingDataFetcherSelector);
+        SlingScalarsProvider slingScalarsProvider = 
mock(SlingScalarsProvider.class);
+        context.registerService(slingScalarsProvider);
+        context.registerInjectActivateService(new 
SimpleGraphQLCacheProvider());
+        context.registerInjectActivateService(new GraphQLServlet(), 
ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES, "a/b/c",
+                "persistedQueries.suffix", "");
+        GraphQLServlet servlet = (GraphQLServlet) 
context.getService(Servlet.class);
+        assertNotNull(servlet);
+
+        context.build().resource("/content/graphql", 
ResourceResolver.PROPERTY_RESOURCE_TYPE, "a/b/c").commit();
+        Resource resource = 
context.resourceResolver().resolve("/content/graphql");
+
+        MockSlingHttpServletResponse response = context.response();
+        MockSlingHttpServletRequest request = new 
MockSlingHttpServletRequest(context.bundleContext());
+
+
+        request.setResource(resource);
+        MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) 
request.getRequestPathInfo();
+        requestPathInfo.setExtension("gql");
+        requestPathInfo.setResourcePath(resource.getPath());
+        requestPathInfo.setSuffix("/persisted");
+        request.setPathInfo("/content/graphql/persisted/hash");
+        servlet.doGet(request, response);
+        assertEquals(400, response.getStatus());
+        assertEquals("Persisted queries are disabled.", 
response.getStatusMessage());
+    }
+
+
+}

Reply via email to