You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2021/06/09 13:27:33 UTC

[sling-whiteboard] branch master updated: Use Folder and Document POJOs (with lazy loading), simplify data fetchers, document the schema

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

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


The following commit(s) were added to refs/heads/master by this push:
     new d6b334a  Use Folder and Document POJOs (with lazy loading), simplify data fetchers, document the schema
d6b334a is described below

commit d6b334aa9ac0c924d4cb321aafee2140bfa05873
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Wed Jun 9 15:27:16 2021 +0200

    Use Folder and Document POJOs (with lazy loading), simplify data fetchers, document the schema
---
 remote-content-api/sample-graphql-api/README.md    |  90 ++++++++++---
 .../remotecontent/contentmodel/Backstage.java      |  42 ++++++
 .../remotecontent/contentmodel/ContentItem.java    |  43 +++++++
 .../contentmodel/ContentItemHeader.java            |  74 +++++++++++
 .../sling/remotecontent/contentmodel/Document.java |  58 +++++++++
 .../sling/remotecontent/contentmodel/Etc.java      |  28 ++++
 .../sling/remotecontent/contentmodel/Folder.java   |  29 +++++
 .../sling/remotecontent/contentmodel/Link.java     |  39 ++++++
 .../samples/graphql/DocumentDataFetcher.java       |  34 +----
 .../samples/graphql/DocumentsDataFetcher.java      |  29 ++---
 .../samples/graphql/FetcherContext.java            |  51 ++++++++
 .../samples/graphql/FolderDataFetcher.java         |  37 +-----
 .../samples/graphql/FoldersDataFetcher.java        |  27 ++--
 .../main/resources/schemas/default/N.GQLschema.jsp | 141 ++++++++++++++++++---
 14 files changed, 587 insertions(+), 135 deletions(-)

diff --git a/remote-content-api/sample-graphql-api/README.md b/remote-content-api/sample-graphql-api/README.md
index acda76e..d6d5f25 100644
--- a/remote-content-api/sample-graphql-api/README.md
+++ b/remote-content-api/sample-graphql-api/README.md
@@ -32,32 +32,90 @@ we don't really care about the details of these node types besides their names.
 
 ## Example GraphQL queries
 
-    { 
-      document(path:"/content/articles/music/eloy-hahn-on-the-system-of-1080p-et-corrupti-aka-xml", selectors: "not, used, sofar") {
-      	path
-        selectors
-        body
+    {
+      folders(limit: 55, after: "L2NvbnRlbnQvYXJ0aWNsZXMvbXVzaWM=") {
+        pageInfo {
+          endCursor
+        }
+        edges {
+          node {
+            path
+            header {
+              resourceType
+              title
+            }
+          }
+        }
       }
     }
-    
+
     {
-      document(path:"/content/wknd/us/en/adventures/riverside-camping-australia", selectors: "not,used,yet") {
-      	path
-        selectors
-        body
+      folder {
+        path
+        header {
+          parent
+          resourceType
+          resourceSuperType
+          links {
+            rel
+            href
+          }
+        }
+      }
+      document {
+        path
+        header {
+          parent
+        }
       }
     }
 
     {
-      documents(query:"//content/*/*") {
+      folder(path: "/apps") {
         path
-        body
+        header {
+          parent
+        }
       }
     }
 
     {
-      documents(query:"//content//*[jcr:contains(.,'wknd')]") {
+      document {
         path
+          header {
+          parent
+          resourceType
+          resourceSuperType
+          title
+          summary
+          description
+          links {
+            href
+            rel
+          }
+          etc
+        }
+        backstage {
+          authoring
+          publishing
+          etc
+        }
+        body
+      }
+    }
+
+    {
+      documents(query: "//content/*/*") {
+        edges {
+          node {
+            path
+            header {
+              parent
+              resourceType 
+              resourceSuperType 
+            }
+          }
+        }
       }
     }
 
@@ -80,12 +138,6 @@ we don't really care about the details of these node types besides their names.
     }
 
     {
-      document(path:"/open-for-all") {
-        body
-      }
-    }
-
-    {
       documents(
         lang:"sql2020",
         query:"""
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Backstage.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Backstage.java
new file mode 100644
index 0000000..84fccd7
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Backstage.java
@@ -0,0 +1,42 @@
+/*
+ * 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.remotecontent.contentmodel;
+
+import java.util.HashMap;
+
+/** Backstage information is meant to provide hints and rules
+ *  to authoring user interfaces and publishing services.
+ */
+public class Backstage extends HashMap<String, Object> {
+    private Etc etc;
+    
+    public Backstage() {
+        put("authoring", "authoring/editor information can come here, free-form");
+        put("publishing", "publishing hints can come here, free-form");
+    }
+
+    public Object getEtc() {
+        if(etc == null) {
+            etc = new Etc();
+            etc.put("info", "...additional information for this Document");
+        }
+        return etc;
+    }
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/ContentItem.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/ContentItem.java
new file mode 100644
index 0000000..723cb85
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/ContentItem.java
@@ -0,0 +1,43 @@
+/*
+ * 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.remotecontent.contentmodel;
+
+import org.apache.sling.api.resource.Resource;
+
+/** Base class for folders and documents */
+public class ContentItem {
+    protected final Resource resource;
+    protected ContentItemHeader header;
+
+    ContentItem(Resource r) {
+        resource = r;
+    }
+
+    public String getPath() {
+        return resource.getPath();
+    }
+
+    public ContentItemHeader getHeader() {
+        if(header == null) {
+            header = new ContentItemHeader(resource);
+        }
+        return header;
+    }
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/ContentItemHeader.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/ContentItemHeader.java
new file mode 100644
index 0000000..3436358
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/ContentItemHeader.java
@@ -0,0 +1,74 @@
+/*
+ * 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.remotecontent.contentmodel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sling.api.resource.Resource;
+
+/** Header for ContentItems */
+public class ContentItemHeader {
+    protected final Resource resource;
+    private List<Link> links;
+
+    ContentItemHeader(Resource r) {
+        resource = r;
+    }
+
+    private void setupLinksIfNeeded() {
+        if(links == null) {
+            links = new ArrayList<>();
+            links.add(new Link("self", resource.getPath()));
+        }
+    }
+
+    public Collection<Link> getLinks() {
+        setupLinksIfNeeded();
+        return Collections.unmodifiableList(links);
+    }
+
+    public String getResourceType() {
+        return resource.getResourceType();
+    }
+
+    public String getResourceSuperType() {
+        return resource.getResourceSuperType();
+    }
+
+    public String getParent() {
+        final Resource parent = resource.getParent();
+        return parent == null ? null : parent.getPath();
+    }
+
+    public String getTitle() {
+        return null;
+    }
+
+    public String getDescription() {
+        return null;
+    }
+
+    public String getSummary() {
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Document.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Document.java
new file mode 100644
index 0000000..4e17508
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Document.java
@@ -0,0 +1,58 @@
+/*
+ * 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.remotecontent.contentmodel;
+
+import org.apache.sling.api.resource.Resource;
+
+/** Base class for folders and documents */
+public class Document extends ContentItem {
+    private Backstage backstage;
+
+    public Document(Resource r) {
+        super(r);
+    }
+
+    public Object getBody() {
+        /*
+        @Reference(target="(" + DocumentTree.TARGET_TYPE + "=map)")
+        private DocumentTree mappingTarget;
+
+        @Reference
+        private DocumentAggregator documentAggregator;
+        
+        // Use the aggregator to build the body
+        if(mappingTarget != null) {
+            final DocumentTree.DocumentNode body = mappingTarget.newDocumentNode();
+            final DocumentAggregator.Options opt = new DocumentAggregator.Options(false, new UrlBuilderStub());
+            documentAggregator.aggregate(r, body, opt);
+            body.close();
+            data.put("body", body.adaptTo(Map.class));
+        }
+        */
+        return "This will be the document body, created using the document aggregator or document-speficic services";
+    }
+
+    public Backstage getBackstage() {
+        if(backstage == null) {
+            backstage = new Backstage();
+        }
+        return backstage;
+    }
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Etc.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Etc.java
new file mode 100644
index 0000000..61b818b
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Etc.java
@@ -0,0 +1,28 @@
+/*
+ * 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.remotecontent.contentmodel;
+
+import java.util.HashMap;
+
+/** Backstage information is meant to provide hints and rules
+ *  to authoring user interfaces and publishing services.
+ */
+public class Etc extends HashMap<String, Object> {
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Folder.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Folder.java
new file mode 100644
index 0000000..c910f63
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Folder.java
@@ -0,0 +1,29 @@
+/*
+ * 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.remotecontent.contentmodel;
+
+import org.apache.sling.api.resource.Resource;
+
+/** Base class for folders and documents */
+public class Folder extends ContentItem {
+    public Folder(Resource r) {
+        super(r);
+    }
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Link.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Link.java
new file mode 100644
index 0000000..2be52a3
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/contentmodel/Link.java
@@ -0,0 +1,39 @@
+/*
+ * 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.remotecontent.contentmodel;
+
+/** Link, with a relation and href */
+public class Link {
+    private final String relation;
+    private final String href;
+
+    public Link(String relation, String href) {
+        this.relation = relation;
+        this.href = href;
+    }
+
+    public String getRel() {
+        return relation;
+    }
+
+    public String getHref() {
+        return href;
+    }
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentDataFetcher.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentDataFetcher.java
index 8dc3332..0673793 100644
--- a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentDataFetcher.java
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentDataFetcher.java
@@ -19,44 +19,18 @@
 
 package org.apache.sling.remotecontent.samples.graphql;
 
-import java.util.Map;
-
-import org.apache.sling.api.resource.Resource;
-import org.apache.sling.documentaggregator.api.DocumentAggregator;
-import org.apache.sling.documentaggregator.api.DocumentTree;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
+import org.apache.sling.remotecontent.contentmodel.Document;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.service.component.annotations.Component;
-import org.osgi.service.component.annotations.Reference;
 
 @Component(service = SlingDataFetcher.class, property = {"name=samples/document"})
-public class DocumentDataFetcher implements SlingDataFetcher<Object> {
-
-    @Reference(target="(" + DocumentTree.TARGET_TYPE + "=map)")
-    private DocumentTree mappingTarget;
-
-    @Reference
-    private DocumentAggregator documentAggregator;
-
-    protected Map<String, Object> toDocument(Resource r) {
-        final Map<String, Object> data = FolderDataFetcher.toDocument(r);
-        //final DocumentAggregator.Options opt = new DocumentAggregator.Options(e.getArgument("debug", false), new UrlBuilderStub());
-
-        // Use the aggregator to build the body
-        if(mappingTarget != null) {
-            final DocumentTree.DocumentNode body = mappingTarget.newDocumentNode();
-            final DocumentAggregator.Options opt = new DocumentAggregator.Options(false, new UrlBuilderStub());
-            documentAggregator.aggregate(r, body, opt);
-            body.close();
-            data.put("body", body.adaptTo(Map.class));
-        }
-        return data;
-    }
+public class DocumentDataFetcher implements SlingDataFetcher<Document> {
 
     @Override
-    public @Nullable Object get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
-        return toDocument(FolderDataFetcher.getTargetResource(e));
+    public @Nullable Document get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
+        return new Document(new FetcherContext(e, false).currentResource);
     }   
 }
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentsDataFetcher.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentsDataFetcher.java
index 4bfb12a..bad235c 100644
--- a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentsDataFetcher.java
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/DocumentsDataFetcher.java
@@ -20,25 +20,23 @@
 package org.apache.sling.remotecontent.samples.graphql;
 
 import java.util.Iterator;
-import java.util.Map;
-import java.util.function.Function;
 
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
-import org.apache.sling.graphql.api.pagination.Cursor;
+import org.apache.sling.graphql.api.pagination.Connection;
 import org.apache.sling.graphql.helpers.GenericConnection;
+import org.apache.sling.remotecontent.contentmodel.Document;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.service.component.annotations.Component;
 
 @Component(service = SlingDataFetcher.class, property = {"name=samples/documents"})
-public class DocumentsDataFetcher extends DocumentDataFetcher {
+public class DocumentsDataFetcher implements SlingDataFetcher<Connection<Document>> {
 
     @Override
-    public @Nullable Object get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
-        final int limit = e.getArgument("limit", 5);
-        final String after = e.getArgument("after", null);
+    public @Nullable Connection<Document> get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
+        final FetcherContext ctx = new FetcherContext(e, false);
 
         // Use a suffix as we might not keep these built-in language in the long term
         final String langSuffix = "2020";
@@ -50,14 +48,11 @@ public class DocumentsDataFetcher extends DocumentDataFetcher {
         lang = lang.replaceAll(langSuffix + "$", "");
         final String query = e.getArgument("query");
 
-        final Iterator<Resource> resultIterator = e.getCurrentResource().getResourceResolver().findResources(query, lang);
-        final Function<Map<String, Object>, String> cursorStringProvider = data -> (String)data.get("path");
-        final Function<Resource, Map<String, Object>> converter = this::toDocument;
-        final Iterator<Map<String, Object>> it = new ConvertingIterator<>(resultIterator, converter);
-        return new GenericConnection.Builder<>(it, cursorStringProvider)
-            .withStartAfter(Cursor.fromEncodedString(after))
-            .withLimit(limit)
+        final Iterator<Resource> resultIterator = ctx.currentResource.getResourceResolver().findResources(query, lang);
+        final Iterator<Document> it = new ConvertingIterator<>(resultIterator, Document::new);
+        return new GenericConnection.Builder<>(it, Document::getPath)
+            .withStartAfter(ctx.afterCursor)
+            .withLimit(ctx.limit)
             .build();
-    }
-    
-}
+    }    
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FetcherContext.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FetcherContext.java
new file mode 100644
index 0000000..63dbbe5
--- /dev/null
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FetcherContext.java
@@ -0,0 +1,51 @@
+/*
+ * 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.remotecontent.samples.graphql;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
+import org.apache.sling.graphql.api.pagination.Cursor;
+
+class FetcherContext {
+    public static final String LIMIT_ARG = "limit";
+    public static final String AFTER_ARG = "after";
+    public static final String PATH_ARG = "path";
+    public static final int DEFAULT_LIMIT = 50;
+
+    public final Resource currentResource;
+    public final Cursor afterCursor;
+    public final int limit;
+
+    FetcherContext(SlingDataFetcherEnvironment e, boolean includePagination) {
+        Resource r = e.getCurrentResource();
+        final String path = e.getArgument(PATH_ARG);
+        if(path != null && !path.isEmpty()) {
+            r = r.getResourceResolver().getResource(path);
+        }
+        currentResource = r;
+        if(includePagination) {
+            afterCursor = Cursor.fromEncodedString(e.getArgument(AFTER_ARG));
+            limit = e.getArgument(LIMIT_ARG, DEFAULT_LIMIT);
+        } else {
+            afterCursor = null;
+            limit = -1;
+        }
+    }
+}
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FolderDataFetcher.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FolderDataFetcher.java
index 494953d..23b0bf5 100644
--- a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FolderDataFetcher.java
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FolderDataFetcher.java
@@ -19,13 +19,9 @@
 
 package org.apache.sling.remotecontent.samples.graphql;
 
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.sling.api.resource.Resource;
-import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
+import org.apache.sling.remotecontent.contentmodel.Folder;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.service.component.annotations.Component;
@@ -33,37 +29,8 @@ import org.osgi.service.component.annotations.Component;
 @Component(service = SlingDataFetcher.class, property = {"name=samples/folder"})
 public class FolderDataFetcher implements SlingDataFetcher<Object> {
 
-    // TODO move to document converter + make body optional
-    static Map<String, Object> toDocument(Resource r) {
-        final Map<String, Object> data = new HashMap<>();
-        data.put("path", r.getPath());
-        data.put("header", toDocumentHeader(r));
-        data.put("properties", r.adaptTo(ValueMap.class));
-        return data;
-    }
-
-    // TODO move to document converter + make body optional
-    static Map<String, Object> toDocumentHeader(Resource r) {
-        final Map<String, Object> header = new HashMap<>();
-        if(r.getParent() != null) {
-            header.put("parent", r.getParent().getPath());
-        }
-        header.put("resourceType", r.getResourceType());
-        header.put("resourceSuperType", r.getResourceSuperType());
-        return header;
-    }
-
-    static Resource getTargetResource(SlingDataFetcherEnvironment e) {
-        Resource result = e.getCurrentResource();
-        String path = e.getArgument("path");
-        if(path != null && !path.isEmpty()) {
-            result = result.getResourceResolver().getResource(path);
-        }
-        return result;
-    }
-
     @Override
     public @Nullable Object get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
-        return toDocument(getTargetResource(e));
+        return new Folder(new FetcherContext(e, false).currentResource);
     }   
 }
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FoldersDataFetcher.java b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FoldersDataFetcher.java
index 25cd7fb..194b7af 100644
--- a/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FoldersDataFetcher.java
+++ b/remote-content-api/sample-graphql-api/src/main/java/org/apache/sling/remotecontent/samples/graphql/FoldersDataFetcher.java
@@ -20,38 +20,33 @@
 package org.apache.sling.remotecontent.samples.graphql;
 
 import java.util.Iterator;
-import java.util.Map;
-import java.util.function.Function;
 
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
-import org.apache.sling.graphql.api.pagination.Cursor;
+import org.apache.sling.graphql.api.pagination.Connection;
 import org.apache.sling.graphql.helpers.GenericConnection;
+import org.apache.sling.remotecontent.contentmodel.Folder;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.service.component.annotations.Component;
 
 @Component(service = SlingDataFetcher.class, property = {"name=samples/folders"})
-public class FoldersDataFetcher implements SlingDataFetcher<Object> {
+public class FoldersDataFetcher implements SlingDataFetcher<Connection<Folder>> {
 
     @Override
-    public @Nullable Object get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
-        final int limit = e.getArgument("limit", 5);
-        final String after = e.getArgument("after", null);
+    public @Nullable Connection<Folder> get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
+        final FetcherContext ctx = new FetcherContext(e, false);
 
-        final Resource context = FolderDataFetcher.getTargetResource(e);
         final String xpathQuery = String.format(
             "/jcr:root%s//element(*, nt:folder) order by jcr:path ascending option(traversal fail)", 
-            context.getPath());
+            ctx.currentResource.getPath());
 
-        final Iterator<Resource> resultIterator = context.getResourceResolver().findResources(xpathQuery, "xpath");
-        final Function<Map<String, Object>, String> cursorStringProvider = data -> (String)data.get("path");
-        final Function<Resource, Map<String, Object>> converter = FolderDataFetcher::toDocument;
-        final Iterator<Map<String, Object>> it = new ConvertingIterator<>(resultIterator, converter);
-        return new GenericConnection.Builder<>(it, cursorStringProvider)
-            .withStartAfter(Cursor.fromEncodedString(after))
-            .withLimit(limit)
+        final Iterator<Resource> resultIterator = ctx.currentResource.getResourceResolver().findResources(xpathQuery, "xpath");
+        final Iterator<Folder> it = new ConvertingIterator<>(resultIterator, Folder::new);
+        return new GenericConnection.Builder<>(it, Folder::getPath)
+            .withStartAfter(ctx.afterCursor)
+            .withLimit(ctx.limit)
             .build();
     }
 }
\ No newline at end of file
diff --git a/remote-content-api/sample-graphql-api/src/main/resources/schemas/default/N.GQLschema.jsp b/remote-content-api/sample-graphql-api/src/main/resources/schemas/default/N.GQLschema.jsp
index 06f5d53..db56f61 100644
--- a/remote-content-api/sample-graphql-api/src/main/resources/schemas/default/N.GQLschema.jsp
+++ b/remote-content-api/sample-graphql-api/src/main/resources/schemas/default/N.GQLschema.jsp
@@ -17,46 +17,151 @@
 * under the License.
 --%>
 
-<%-- N plane schema: Navigation --%>
+<%-- 
+N plane schema: Navigation
+There's no real need for this to be a JSP script, we might want to
+create a "text passthrough" script engine for such things.
+--%>
+
+"""
+Some fields use this Scalar to provide unstructured or semi-structured data
+"""
 scalar Object
 
 type Query {
+  """ 
+  Query a single Folder.
+  If not specified, the path defaults to the Resource which receives the query request.
+  """
   folder(path: String) : Folder @fetcher(name:"samples/folder")
+
+  """ 
+  Paginated query for multiple Folders.
+  """
   folders(path: String, limit: Int, after: String) : FolderConnection @connection(for: "Folder") @fetcher(name:"samples/folders")
-  document(path : String, selectors : [String], debug : Boolean) : Document @fetcher(name:"samples/document")
-  documents(lang: String, query : String, debug: Boolean, limit: Int, after: String) : DocumentConnection @connection(for: "Document") @fetcher(name:"samples/documents")
-}
 
-type Mutation {
-  command(lang: String, script: String) : CommandResult @fetcher(name:"samples/command")
+  """
+  Query a single Document. If not specified, the path defaults to the Resource which receives the query request
+  """
+  document(path : String) : Document @fetcher(name:"samples/document")
+
+  """ 
+  Paginated query for multiple documents.
+  'lang' indicates the query language - TODO provide a query to list those languages and their documentation
+  """
+  documents(lang: String, query : String, limit: Int, after: String) : DocumentConnection @connection(for: "Document") @fetcher(name:"samples/documents")
 }
 
+"""
+A Folder can contain other Folders or Documents
+""" 
 type Folder {
-  path : String
-  header : DocumentHeader
+  path : ID!
+  header : ContentItemHeader!
 }
 
-type DocumentHeader {
+"""
+Common header for Folders and Documents
+""" 
+type ContentItemHeader {
+  """ path of the parent Folder or Document """
   parent : String
+
+  """ The resource type of this document, can be used for example to select publishing templates """
   resourceType : String
+
+  """ The resource supertype provides a simple form of inheritance, for templating fallbacks for example """
   resourceSuperType : String
+
+  """ The title of this document, if supplied """
+  title : String
+
+  """ The description of this document, if supplied """
+  description : String
+
+  """ The summary of this document, if supplied """
+  summary : String
+  links : [Link]
+
+  """
+  etc can contain any additional information, as an unstructured Object scalar
+  """
+  etc : Object
 }
 
+"""
+A Document represents content that's usually meant for authoring and publishing
+"""
 type Document {
-  path : String
-  header : DocumentHeader
-  properties : Object
+  path : ID!
+  header : ContentItemHeader!
+  backstage : Backstage
+
+  """ 
+  The document's body is unstructured, usually generated by a document aggregator service.
+  Fields like 'resourceType' in that content can help rendering or applying UI logic to it.
+  """
   body : Object
 }
 
-type CommandResult {
-  success: Boolean
-  output: String
-  help: String
-  links: [Link]
+"""
+Backstage is for data related to authoring or publishing content:
+authoring rules, publishing hints etc.
+"""
+
+type Backstage {
+  authoring : Object
+  publishing : Object
+
+  """
+  etc can contain any additional information, as an unstructured Object scalar
+  """
+  etc : Object
 }
 
+"""
+A link with its relationship and href
+""" 
 type Link {
+  """
+  The link relationship
+  TODO defined standard values for this
+  """
   rel: String
-  url: String
+
+  """
+  The link's href, as would be used in HTML
+  """
+  href: String!
+}
+
+"""
+Commands can be sent using this Mutation
+"""
+type Mutation {
+  """ 
+  'lang' is the command language - TODO provide a query that lists languages with their help text
+  'script' is the script to execute, in the language indicated by 'lang'
+  """  
+  command(lang: String, script: String) : CommandResult @fetcher(name:"samples/command")
+}
+
+"""
+The result of executing a command
+"""
+type CommandResult {
+  """ true if the command was successful (TODO use status/error codes?) """
+  success: Boolean!
+
+  """ The command output, as text """
+  output: String
+
+  """ Optional help text for this command """
+  help: String
+
+  """ 
+  Links can point to resources that the command created, for example,
+  or to the documentation of the command itself, or its language.
+  """
+  links: [Link]
 }
\ No newline at end of file