You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2021/05/17 13:08:51 UTC

[sling-org-apache-sling-graphql-core] branch master updated: SLING-10375 - Sling-specific GraphQL directives should be built in

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5d43e13  SLING-10375 - Sling-specific GraphQL directives should be built in
5d43e13 is described below

commit 5d43e133d6c0d29feb7499e28cb9a3f617b5a8a4
Author: Radu Cotescu <17...@users.noreply.github.com>
AuthorDate: Mon May 17 15:08:41 2021 +0200

    SLING-10375 - Sling-specific GraphQL directives should be built in
    
    * added the custom directives supported by this module to the
    TypeDefinitionRegistry
    * documented the directives in the README
---
 README.md                                          |  77 +++++++++------
 .../sling/graphql/core/directives/Directives.java  | 107 +++++++++++++++++++++
 .../graphql/core/engine/DefaultQueryExecutor.java  |   7 ++
 src/test/resources/failing-schema.txt              |   7 --
 .../resources/failing-type-resolver-schema.txt     |   7 --
 .../apps/graphql/test/one/GQLschema.jsp            |  14 ---
 src/test/resources/paginated-humans-schema.txt     |  14 ---
 src/test/resources/scalars-schema.txt              |   9 +-
 src/test/resources/test-schema-selected-foryou.txt |   9 +-
 src/test/resources/test-schema.txt                 |  18 +---
 10 files changed, 167 insertions(+), 102 deletions(-)

diff --git a/README.md b/README.md
index e3d0c57..d80f8a2 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,6 @@ need to use their APIs directly.
 The [GraphQL sample website](https://github.com/apache/sling-samples/tree/master/org.apache.sling.graphql.samples.website)
 provides usage examples and demonstrates using GraphQL queries (and Handlebars templates) on both the server and
 client sides.
-
-> As I write this, work is ongoing at [SLING-9550](https://issues.apache.org/jira/browse/SLING-9550) to implement custom 
-> GraphQL Scalars
  
 ## Supported GraphQL endpoint styles
 
@@ -87,24 +84,30 @@ schemas dynamically, taking request selectors into account.
 Unless you have specific needs not covered by this mechanism, there's no need to implement your
 own `SchemaProvider` services.
 
-## SlingDataFetcher selection with Schema Directives
+## Built-in directives
 
-The GraphQL schemas used by this module can be enhanced using
-[schema directives](http://spec.graphql.org/June2018/#sec-Language.Directives)
-(see also the [Apollo docs](https://www.apollographql.com/docs/graphql-tools/schema-directives/) for how those work)
-that select specific `SlingDataFetcher` services to return the appropriate data.
+Version 0.0.10 of the Apache Sling GraphQL Core introduces the concept of built-in directives. The `@fetcher` and
+`@resolver` directives were supported before as well as part of the schema definition. However, starting from version
+0.0.10 their schema definition is redundant, since all schemas will now be automatically extended to provide support
+for the built-in directives.
 
-A default data fetcher is used for types and fields which have no such directive.
+### SlingDataFetcher selection using the `@fetcher` directive
 
-Here's a simple example, the test code has more:
+The following built-in `@fetcher` directive is defined by this module:
 
     # This directive maps fields to our Sling data fetchers
     directive @fetcher(
-        name : String,
+        name : String!,
         options : String = "",
         source : String = ""
     ) on FIELD_DEFINITION
 
+A field using the built-in `@fetcher` directive allows selecting a specific `SlingDataFetcher` service to return the appropriate data.
+
+Fileds which do not have such a directive will be retrieved using the default data fetcher.
+
+Here's a simple example, the test code has more:
+
     type Query {
       withTestingSelector : TestData @fetcher(name:"test/pipe")
     }
@@ -123,21 +126,20 @@ which have Java package names that start with `org.apache.sling`.
 The `<options>` and `<source>` arguments of the directive can be used by the
 `SlingDataFetcher` services to influence their behavior.
 
-## SlingTypeResolver selection with Schema Directives
-
-The GraphQL schemas used by this module can be enhanced using
-[schema directives](http://spec.graphql.org/June2018/#sec-Language.Directives)
-(see also the [Apollo docs](https://www.apollographql.com/docs/graphql-tools/schema-directives/) for how those work)
-that select specific `SlingTypeResolver` services to return the appropriate GraphQL object type using Unions.
+### SlingTypeResolver selection using the `@resolver` directive
 
-Here's a simple example, the test code has more:
+The following built-in `@resolver` directive is defined by this module:
 
     # This directive maps the corresponding type resolver to a given Union
     directive @resolver(
-        name: String, 
+        name: String!, 
         options: String = "", 
         source: String = ""
-    ) on UNION
+    ) on UNION | INTERFACE
+
+A `Union` or `Interface` type can provide a `@resolver` directive, to select a specific `SlingTypeResolver` service to return the appropriate GraphQL object type.
+
+Here's a simple example, the test code has more:
 
     union TestUnion @resolver(name : "test/resolver", source : "TestUnion") = Type_1 | Type_2 | Type_3 | Type_4
 
@@ -151,18 +153,35 @@ which have Java package names that start with `org.apache.sling`.
 The `<options>` and `<source>` arguments of the directive can be used by the
 `SlingTypeResolver` services to influence their behavior.
 
-## Result Set Pagination
+## Result Set Pagination using the `@connection` and `@fetcher` directives
+
+This module implements support for the [Relay Cursor Connections](https://relay.dev/graphql/connections.htm)
+specification, via the built-in `@connection` directive, coupled with a `@fetcher` directive. The built-in `@connection`
+directive has the following definition:
+
+    directive @connection(
+      for: String!
+    ) on FIELD_DEFINITION
+
+With this in mind, your schema that supports pagination can look like:
 
-The [GenericConnection](./src/main/java/org/apache/sling/graphql/core/helpers/pagination/GenericConnection.java) class provides support
-for paginated results, following the [Relay Cursor Connections](https://relay.dev/graphql/connections.htm) specification.
+    type Query {
+        paginatedHumans (after : String, limit : Int) : HumanConnection @connection(for: "Human") @fetcher(name:"humans/connection")
+    }
+
+    type Human {
+        id: ID!
+        name: String!
+        address: String
+    }
 
-With this utility class, you just need to supply an `Iterator` on your data, a function to generate a string that represents the cursor
-for a given object, and optional parameters to control the page start and length.
+The [GenericConnection](./src/main/java/org/apache/sling/graphql/core/helpers/pagination/GenericConnection.java) class,
+together with the [`org.apache.sling.graphql.api.pagination`](./src/main/java/org/apache/sling/graphql/api/pagination) API
+provide support for paginated results. With this utility class, you just need to supply an `Iterator` on your data, a
+function to generate a string that represents the cursor for a given object, and optional parameters to control the
+page start and length.
 
-The [QueryDataFetcherComponent](./src/test/java/org/apache/sling/graphql/core/mocks/QueryDataFetcherComponent.java) test class has a 
-concrete example. The below code is sufficient to produce a paginated result according to the Relay spec, assuming the GraphQL schema
-contains the required types. We are working on a schema directive to generate those connection types automatically, but for now they
-can be added manually to a schema, like [the one used in this test](./src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp).
+The [QueryDataFetcherComponent](./src/test/java/org/apache/sling/graphql/core/mocks/QueryDataFetcherComponent.java) provides a usage example: 
 
     // fake test data simulating a query
     final List<Resource> data = new ArrayList<>();
diff --git a/src/main/java/org/apache/sling/graphql/core/directives/Directives.java b/src/main/java/org/apache/sling/graphql/core/directives/Directives.java
new file mode 100644
index 0000000..7cfeeec
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/directives/Directives.java
@@ -0,0 +1,107 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.directives;
+
+import graphql.introspection.Introspection;
+import graphql.language.Description;
+import graphql.language.DirectiveDefinition;
+import graphql.language.DirectiveLocation;
+import graphql.language.InputValueDefinition;
+import graphql.language.NonNullType;
+import graphql.language.StringValue;
+import graphql.language.TypeName;
+
+public class Directives {
+
+    private Directives() {}
+    
+    public static final String TYPE_STRING = "String";
+
+    public static final DirectiveDefinition CONNECTION = DirectiveDefinition.newDirectiveDefinition()
+            .name("connection")
+            .directiveLocation(DirectiveLocation.newDirectiveLocation().name(Introspection.DirectiveLocation.FIELD_DEFINITION.name()).build())
+            .description(new Description("Marks a connection type according to the Relay specification.", null, false))
+            .inputValueDefinition(
+                    InputValueDefinition.newInputValueDefinition()
+                            .name("for")
+                            .description(new Description("The type for which the connection is created.", null, false))
+                            .type(NonNullType.newNonNullType(TypeName.newTypeName(TYPE_STRING).build()).build())
+                            .build()
+            )
+            .build();
+
+    public static final DirectiveDefinition FETCHER = DirectiveDefinition.newDirectiveDefinition()
+            .name("fetcher")
+            .directiveLocation(DirectiveLocation.newDirectiveLocation().name(Introspection.DirectiveLocation.FIELD_DEFINITION.name()).build())
+            .description(new Description("Maps a field to a SlingDataDetcher.", null, false))
+            .inputValueDefinition(
+                    InputValueDefinition.newInputValueDefinition()
+                            .name("name")
+                            .description(new Description("The name with which the SlingDataFetcher was registered.", null, false))
+                            .type(NonNullType.newNonNullType(TypeName.newTypeName(TYPE_STRING).build()).build())
+                            .build()
+            )
+            .inputValueDefinition(
+                    InputValueDefinition.newInputValueDefinition()
+                            .name("options")
+                            .description(new Description("Options passed to the SlingDataFetcher.", null, false))
+                            .type(TypeName.newTypeName(TYPE_STRING).build())
+                            .defaultValue(new StringValue(""))
+                            .build()
+            )
+            .inputValueDefinition(
+                    InputValueDefinition.newInputValueDefinition()
+                            .name("source")
+                            .description(new Description("Source information passed to the SlingDataFetcher.", null, false))
+                            .type(TypeName.newTypeName(TYPE_STRING).build())
+                            .defaultValue(new StringValue(""))
+                            .build()
+            )
+            .build();
+
+    public static final DirectiveDefinition RESOLVER = DirectiveDefinition.newDirectiveDefinition()
+            .name("resolver")
+            .directiveLocation(DirectiveLocation.newDirectiveLocation().name(Introspection.DirectiveLocation.INTERFACE.name()).build())
+            .directiveLocation(DirectiveLocation.newDirectiveLocation().name(Introspection.DirectiveLocation.UNION.name()).build())
+            .description(new Description("Maps a type to a SlingTypeResolver.", null, false))
+            .inputValueDefinition(
+                    InputValueDefinition.newInputValueDefinition()
+                            .name("name")
+                            .description(new Description("The name with which the SlingTypeResolver was registered.", null, false))
+                            .type(NonNullType.newNonNullType(TypeName.newTypeName(TYPE_STRING).build()).build())
+                            .build()
+            )
+            .inputValueDefinition(
+                    InputValueDefinition.newInputValueDefinition()
+                            .name("options")
+                            .description(new Description("Options passed to the SlingTypeResolver.", null, false))
+                            .type(TypeName.newTypeName(TYPE_STRING).build())
+                            .defaultValue(new StringValue(""))
+                            .build()
+            )
+            .inputValueDefinition(
+                    InputValueDefinition.newInputValueDefinition()
+                            .name("source")
+                            .description(new Description("Source information passed to the SlingTypeResolver.", null, false))
+                            .type(TypeName.newTypeName(TYPE_STRING).build())
+                            .defaultValue(new StringValue(""))
+                            .build()
+            )
+            .build();
+}
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java b/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
index 843f281..d34e036 100644
--- a/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
+++ b/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
@@ -37,6 +37,7 @@ import org.apache.sling.graphql.api.SlingGraphQLException;
 import org.apache.sling.graphql.api.SlingTypeResolver;
 import org.apache.sling.graphql.api.engine.QueryExecutor;
 import org.apache.sling.graphql.api.engine.ValidationResult;
+import org.apache.sling.graphql.core.directives.Directives;
 import org.apache.sling.graphql.core.hash.SHA256Hasher;
 import org.apache.sling.graphql.core.scalars.SlingScalarsProvider;
 import org.apache.sling.graphql.core.schema.RankedSchemaProviders;
@@ -365,6 +366,12 @@ public class DefaultQueryExecutor implements QueryExecutor {
                 if (!newHash.equals(oldHash)) {
                     resourceToHashMap.put(resourceToHashMapKey, newHash);
                     TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
+                    typeRegistry.add(Directives.CONNECTION);
+                    typeRegistry.add(Directives.FETCHER);
+                    typeRegistry.add(Directives.RESOLVER);
+                    for (ObjectTypeDefinition typeDefinition : typeRegistry.getTypes(ObjectTypeDefinition.class)) {
+                        handleConnectionTypes(typeDefinition, typeRegistry);
+                    }
                     hashToSchemaMap.put(newHash, typeRegistry);
                     return typeRegistry;
                 }
diff --git a/src/test/resources/failing-schema.txt b/src/test/resources/failing-schema.txt
index a559d0f..e5642e0 100644
--- a/src/test/resources/failing-schema.txt
+++ b/src/test/resources/failing-schema.txt
@@ -15,13 +15,6 @@
 # * specific language governing permissions and limitations
 # * under the License.
 
-# This directive maps fields to our Sling data fetchers
-directive @fetcher(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on FIELD_DEFINITION
-
 # Schema used to test invalid fetcher names
 type Query {
     currentResource : SlingResource @fetcher(name:"echoNS/echo")
diff --git a/src/test/resources/failing-type-resolver-schema.txt b/src/test/resources/failing-type-resolver-schema.txt
index 7451b3a..14b60e2 100644
--- a/src/test/resources/failing-type-resolver-schema.txt
+++ b/src/test/resources/failing-type-resolver-schema.txt
@@ -15,13 +15,6 @@
 # * specific language governing permissions and limitations
 # * under the License.
 
-# This directive maps fields to our Sling data fetchers
-directive @resolver(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on UNION
-
 # GraphQL Schema used for our tests
 type Query {
     currentResource : SlingResource @fetcher(name:"echoNS/echo")
diff --git a/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp b/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp
index fca94ba..405d626 100644
--- a/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp
+++ b/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp
@@ -17,20 +17,6 @@
 * under the License.
 --%>
 
-# This directive maps fields to our Sling data fetchers
-directive @fetcher(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on FIELD_DEFINITION
-
-# This directive will generate the additional types for a Connection,
-# according to the Relay Connections specification from
-# https://relay.dev/graphql/connections.htm
-directive @connection(
-    for : String
-) on FIELD_DEFINITION
-
 type Query {
   oneSchemaResource : SlingResource @fetcher(name:"test/pipe" source:"$")
   oneSchemaQuery : SlingResourceConnection @connection(for: "SlingResource") @fetcher(name:"test/query")
diff --git a/src/test/resources/paginated-humans-schema.txt b/src/test/resources/paginated-humans-schema.txt
index 3fecc3d..19ee626 100644
--- a/src/test/resources/paginated-humans-schema.txt
+++ b/src/test/resources/paginated-humans-schema.txt
@@ -15,20 +15,6 @@
 # * specific language governing permissions and limitations
 # * under the License.
 
-# This directive maps fields to our Sling data fetchers
-directive @fetcher(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on FIELD_DEFINITION
-
-# This directive will generate the additional types for a Connection,
-# according to the Relay Connections specification from
-# https://relay.dev/graphql/connections.htm
-directive @connection(
-    for : String
-) on FIELD_DEFINITION
-
 type Query {
     paginatedHumans (after : String, limit : Int) : HumanConnection @connection(for: "Human") @fetcher(name:"humans/connection")
 }
diff --git a/src/test/resources/scalars-schema.txt b/src/test/resources/scalars-schema.txt
index 75851ff..fb5735f 100644
--- a/src/test/resources/scalars-schema.txt
+++ b/src/test/resources/scalars-schema.txt
@@ -15,13 +15,6 @@
 # * specific language governing permissions and limitations
 # * under the License.
 
-# This directive maps fields to our Sling data fetchers
-directive @fetcher(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on FIELD_DEFINITION
-
 scalar URL
 scalar UppercaseString
 
@@ -32,4 +25,4 @@ type Query {
 type AddressInfo {
     hostname: UppercaseString
     url: URL
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/test-schema-selected-foryou.txt b/src/test/resources/test-schema-selected-foryou.txt
index 0c22a9d..d34d190 100644
--- a/src/test/resources/test-schema-selected-foryou.txt
+++ b/src/test/resources/test-schema-selected-foryou.txt
@@ -15,13 +15,6 @@
 # * specific language governing permissions and limitations
 # * under the License.
 
-# This directive maps fields to our Sling data fetchers
-directive @fetcher(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on FIELD_DEFINITION
-
 # GraphQL Schema used for our tests
 # when specific request selectors are used
 type Query {
@@ -31,4 +24,4 @@ type Query {
 type SlingResource { 
     fortyTwo: Int @fetcher(name:"test/fortyTwo")
     path: Int @fetcher(name:"test/fortyTwo")
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/test-schema.txt b/src/test/resources/test-schema.txt
index e7fb1b0..02dc7d5 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/test-schema.txt
@@ -15,21 +15,9 @@
 # * specific language governing permissions and limitations
 # * under the License.
 
-# This directive maps fields to our Sling data fetchers
-directive @fetcher(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on FIELD_DEFINITION
-
-# This directive to maps types
-directive @resolver(
-    name : String,
-    options : String = "",
-    source : String = ""
-) on UNION | INTERFACE
-
-# GraphQL Schema used for our tests
+"""
+GraphQL Schema used for our tests
+"""
 type Query {
     currentResource : SlingResource @fetcher(name:"echoNS/echo")