Using GraphQL to perform queries

November 11, 2022

1 Introduction

GraphQL provides an alternative to a REST / CRUD model that allows more flexibility in the query, manipulation of the contents and services called. It provides a full framework, with cache capabilities and extensions, all fully integrated within DX.
Please consult http://graphql.org/learn/ to understand GraphQL concepts.

2 OSGi bundles

DX comes with several GraphQL OSGi bundles.

2.1 Graphql-java

https://github.com/Jahia/graphql-java

This is the Java implementation of GraphQL specifications

2.2 Graphql-java-servlet

https://github.com/Jahia/graphql-java-servlet
This bundle provides the following GraphQL endpoint:

/{context}/modules/graphql

2.3 Graphql-java.annotations

https://github.com/Jahia/graphql-java-annotations
This bundle provides annotations for GraphQL integration.

2.4 GraphiQL

GraphiQL is a graphical tool that helps building GraphQL queries or mutations.
The tool is available here: http://{hostname}:{port}/{context}/modules/graphql-dxm-provider/tools/graphiql.jsp

3 GraphQL JCR Implementation

We provide a jcr implementation that allows to perform queries and mutations against the repository.

3.1 Generalities on DX GraphQL Schema

3.1.1 Session

The session (logged user) used for queries is the JCR Session from the current HTTP request. If you not logged in into DX, a guest user is used for queries (in such case queries in EDIT workspace will bring errors, as guest user is not allowed to read content in that workspace).

Like for the queries, the session (logged user) used for mutations is the JCR Session from the current HTTP request. If you are not logged in into DX, a guest user is used for mutations (in such case most of the mutations will bring errors, as guest user is not allowed to modify the JCR contents).

3.1.2 Workspace

All JCR queries and mutations are prefixed by

jcr (workspace: LIVE|EDIT) {
}

workspace is an optional parameter that specifies the workspace on which the query will be executed. If not set, the EDIT (default) workspace is used.

JCR session saves are done automatically at the end of each mutation { jcr } field. So you can do multiple operations inside the same jcr field, and only one session save will be performed at the end. By doing so you are sure that nothing is persisted in case something went wrong inside your jcr block. If you need multiple saves, you can do multiple jcr blocks in the same mutation.

If you want to retrieve all the nodes that have been modified by a mutation you can use the field: modifiedNodes available inside the mutation { jcr } field.

3.1.3 Internationalization

The API gives a global view of the nodes and allows to access multiple languages at the same time. As opposed to JCR/REST API, there’s no concept of locale in a session - a query can ask properties values in english and french at the same time. When a language is required for resolving a specific field like a property, it will be passed as a language argument.

Example:

{
    jcr(workspace: LIVE) {
        nodeByPath(path: "/sites/digitall/home") {
            displayName(language: "en")
        }
    }
}

3.1.4 Authentication and Authorization

GraphQL uses HTTP and relies on standard DX authentication valve to get the current user.

As we are using the JCR session from the http session, permissions on nodes are natively supported.

In addition, some graphQL fields may or may not be allowed for a user. This is based on security filter configuration. You should use a dedicated security filter configuration, named  org.jahia.modules.api.permissions-<id>.cfg. A default configuration is also provided with the GraphQL module :

/digital-factory-data/karaf/etc/org.jahia.modules.api.permissions-gql.cfg

If you are not familiar with security filter, first check the documentation to understand the concepts of rules and restrictions. Security filter can be set up to filter specific fields, based on user permissions or tokens. The "api" field of security rule can take values following the format graphql.<type>.<field>, with <type> and <field> optionals. For example, let's create a rule for the root field "nodesByQuery". Root fields are actually fields of the "Query" type. So, you will have to use :

permission.rule1.api=graphql.Query.nodesByQuery

It's also possible to apply a rule on all fields of a type : 

permission.rule2.api=graphql.GqlNode

Or on all Graphql calls, by just specifying "graphql" in the api field. 

You may only want to apply the rule on only some nodes that will be returned by nodesByQuery - the user will be allowed to execute the query, but will only see nodes matching a specific path or node types. 

permission.rule1.path=/sites/example/content/

If no path or node type is specified (or if the root node of the JCR matches the path and node types), the rule will match all call on the field, whatever the result is.

3.1.5 Relay standard support

When a field returns a potentially long list of items, which may need to be paginated, it can return a Connection object, as defined in https://facebook.github.io/relay/graphql/connections.htm. Connection support cursor-based and/or offset-based pagination. Note that in the relay model, items are called nodes, which can be confusing with jcr nodes - here nodes can be any type of objects.

Any field returning a connection accepts the standard arguments : first,after,last, before, afterOffset, as described in https://facebook.github.io/relay/graphql/connections.htm#sec-Arguments

afterOffset / beforeOffset are added to be able use offset-based pagination and go directly to a specific page. They are used the same way as after / before, but specifying an offset instead of a cursor. In this document, all these arguments will be referred as : (...connection arguments...)

The PageInfo type is common to all connections, and is defined as :

# Information about pagination in a connection.
type PageInfo {
    # When paginating forwards, are there more items?
    hasNextPage: Boolean!

    # When paginating backwards, are there more items?
    hasPreviousPage: Boolean!

    # When paginating backwards, the cursor to continue.
    startCursor: String

    # When paginating forwards, the cursor to continue.
    endCursor: String

    # Number of nodes in the current page
    nodesCount: Int

    # The total number of items in the connection
    totalCount: Int
}

nodesCount return the number of items in the current page, totalNodesCount return the total number of items in the list (may be null if not available).

All Connection and Edge types follow the following template :

type XxxConnection {
    edges: [XxxEdge]
    pageInfo: PageInfo
    nodes: [Xxx]
}

type XxxEdge {
    node: Xxx
    cursor: String
    offset: Int
}

The nodes field is an additional non-standard shortcut to edges { node }. It directly returns the list of items, which is usually enough even for pagination as you have startCursor/startOffset in the PageInfo object. offset in Edge return the offset of this particular edge. Connection types won’t be described everytime in the schema and are assumed to follow this schema, when a field return a ...Connection type.

3.1.6 Field filtering and sorting

Some fields returning a Connection or a list of items can take a fieldFilter and/or a fieldSorter parameter. The fieldFilter allows to filter the list of results based on any GraphQL field (or a combination of fields) available on the current type.

The fieldSorter allows to sort by the value of a field.

3.2 Generic JCR nodes representation

3.2.1 JCRNode object

All JCR nodes are represented by a graphQL JCRNode interface type. Base properties (uuid, name, path) are available as fields. Parent can be accessed through a dedicated field.

A base implementation GenericJCRNode of the JCRNode interface is provided. Other implementation, more specialized can be added by other modules.

Properties can be accessed by the properties fields. An optional names argument allows to filter the requested properties by name - otherwise, all properties are returned.

Children nodes are accessed with the nodes field. They can be filtered by using optional arguments :

  • names : only nodes matching one of the provided names pattern will be returned.
  • type : test the node types of the node. Only node with any/all of the specified type will be returned.
  • property : test property existence/value. Only nodes having a specific property, or having a specific value for this property, will be returned.

Descendant nodes can be accessed with the descendants field. The descendants can be filtered by type or property, as children.

ancestors return the list of all ancestors, from root to the direct parent. If upToPath is specified, the returned list will start at this node instead of root node.

Children and descendants fields use the Connection pagination model.

3.2.2 Aggregations

The JCRNodeConnnection type contains an aggregation field, that can be used to calculate an aggregation on a list of JCRNode items and their properties. The aggregation avg / min / max / sum can be used on any numeric property (or date property). The count aggregation will return the total number of values for the specified property.

3.2.3 Queries

All JCR queries are done within a JCRQuery object, which defines in which workspace the operations are done. The provider adds the following fields for Query :

extends type Query {
    # JCR Queries
    jcr(workspace: Workspace): JCRQuery
}

The JCRQuery type contains fields to get nodes by id, path, query, ...

:
type JCRQuery {
    # Get GraphQL representation of a node by its UUID
    nodeById(uuid: String!): JCRNode!

    # Get GraphQL representation of a node by its path
    nodeByPath(path: String!): JCRNode!

    # Get GraphQL representations of multiple nodes by their UUIDs
    nodesById(uuids: [String!]!): [JCRNode]!

    # Get GraphQL representations of multiple nodes by their paths
    nodesByPath(paths: [String!]!): [JCRNode]!

    # Get GraphQL representations of nodes using a query language supported by JCR
    nodesByQuery(query: String!, queryLanguage: QueryLanguage = SQL2, ...connection arguments...): JCRNodeConnection
}

3.2.4 Mutations

Mutations are provided to update or create nodes. Nested mutations can be used to do different operations on nodes and properties, which can then be easily extended.

At the first level, all JCR operations are done inside a JCRMutation object - a session.save() is done once all nested mutations have been resolved.

JCRMutation objects provide fields to do operations at JCR session level :

  • Adds new node with addNode field. This returns a JCRNodeMutation object, on which subsequent operations can be done on the added node.
  • Adds multiple nodes in batch by passing a full JSON structure in input to addNodesBatch field. The same thing can be achieved by using addNode field and sub fields multiple times, but this one provides the ability to create a node and set its properties, mixin types and optional sub nodes by passing a single JSON object.
  • Select existing nodes for edition with mutateNode, mutateNodes, and mutateNodesByQuery fields. These mutations fields returns a JCRNodeMutation object (or a list of JCRNodeMutation).
  • delete / markForDeletion / unmarkForDeletion with deleteNode, markNodeForDeletion and unmarkNodeForDeletion fields These fields take a pathOrId (parentPathOrId) parameter when needed, which can be used indifferently as an absolute path or a node uuid.

The JCRNodeMutation contains operations that can be done on a JCR node. The base API provides fields to edit properties, children, mixin, and also move, delete or rename the node - but it can be extended in other modules to do more complex operations like publication, versioning, locking, or custom operation.

  • Edit properties with mutateProperty and mutateProperties fields, which return a JCRPropertyMutation object (or a list of JCRPropertyMutation objects)
  • Edit descendants with mutateDescendant, mutateChildren and mutateDescendants fields, which return a JCRNodeMutation object on the sub node.
  • Add children with addChild, similar to the addNode at JCRMutation level, without specifying the parent
  • Add children in batch can also be done with addChildrenBatch , like the addNodesBatch at JCRMutation level.
  • Add/remove mixin with addMixins / removeMixins fields
  • Move or rename the node with move / rename fields
  • Delete, mark for deletion or unmark for deletion with delete / markForDeletion / unmarkForDeletion fields
  • Reorder children with reorderChildren field, based on a list of names. Children not listed in parameter are ignored in the reordering.
  • Add / set properties in batch with setPropertiesBatch. If properties were already existing, the value is entirely replaced with the one passed in parameter

Note that most operations at JCRMutation levels are shortcuts :

  • addNode is equivalent to JCRMutation.mutateNode.addChild
  • deleteNode is equivalent to JCRMutation.mutateNode.delete
  • undeleteNode is equivalent to JCRMutation.mutateNode.undelete

The JCRPropertyMutation contains operations on properties. Fields are provided to replace value(s), add/remove value(s) to multi-valued properties, or remove the property.

3.3 Node types

Nodetypes definitions have their equivalent in graphQL schema, allowing to query any metadata of a nodetype. The types maps the properties of the ExtendedNodeType / ExtendedItemDefinition / ExtendedNodeDefinition / ExtendedPropertyDefinition classes. Fields are available on node / properties :

extend type JCRNode {
    primaryNodeType: JCRNodeType!
    mixinTypes: [JCRNodeType]
    isNodeType(type: NodeTypeCriteria!): boolean
    definition: JCRNodeDefinition
}
extend type JCRProperty {
    definition: JCRPropertyDefinition
}

Properties and child node definitions can be queried by name - if no name is passed as argument, all items are returned. Using * as name will return all unstructured item definitions.

3.4 Query nodes

A predefined set of input object allows to create a comprehensive query, without the need of creating a JCR-SQL2 query. They are used with the nodesByQuery field on JCRQuery :

extend type Query {
    nodesByQuery(queryInput: JCRNodesQueryInput, ...connection arguments...): JCRNodeConnection
}

Examples

Query nodes by property value and base path :

nodesByQuery(queryInput:{
    nodeType:"jnt:bigText",
    basePaths:["/sites/digitall/home"],
    language:"en",
    constraint:{property:"text", equals: "test"}
})

is equivalent to :

SELECT * from [jnt:bigText] where isdescendantnode("/sites/digitall/home") and [text]="test"

Query nodes by node name and direct parent path:

nodesByQuery(queryInput:{
    nodeType:"jnt:bigText",
    basePaths:["/sites/digitall/home"],
    includeDescendants: false
    constraint:{function: NODE_NAME, equals: "test"}
})

is equivalent to :

SELECT * from [jnt:bigText] where ischildnode("/sites/digitall/home") and name()="test"

Query nodes by property value with lower case modifier and multiple base paths :

nodesByQuery(queryInput:{
    nodeType:"jnt:bigText",
    basePaths:["/sites/digitall/home/about", "/sites/digitall/home/news"],
    constraint:{property:"author", function: LOWER_CASE, equals: "user"}
})

is equivalent to :

SELECT * from [jnt:bigText] where (isdescendantnode("/sites/digitall/home/about") or isdescendantnode("/sites/digitall/home/news")) and lowercase("author")="user"

Query nodes by fulltext on a property :

nodesByQuery(queryInput:{
    nodeType:"jnt:bigText",
    language:"en",
    constraint:{property:"text", contains: "test"}
})

is equivalent to :

SELECT * from [jnt:bigText] where contains("text","test")

Query nodes by fulltext on any property :

nodesByQuery(queryInput:{
    nodeType:"jnt:bigText",
    language:"en",
    constraint:{contains: "test"}
})

is equivalent to :

SELECT * from [jnt:bigText] contains("*","test")

Query nodes with multiple constraints :

nodesByQuery(queryInput:{
    nodeType:"jnt:bigText",
    language:"en",
    constraint:{
        any: [
            {function:NODE_NAME, equals: "test"},
            {property:"text", contains: "test"}
        ]
    }
})

is equivalent to :

SELECT * from [jnt:bigText] where (name()="test" or [text]="test")

3.5 Lock

It's possible to get lock information on node, and use mutations to lock/unlock nodes, or clear all locks.
extend type JCRNode {
    lockInfo: LockInfo
}

extend type JCRNodeMutation {
    lock(type: String, recursive: Boolean): Boolean
    unlock(type: String, recursive: Boolean force: Boolean): Boolean
}

3.6 Publication

The API allows to publish a node and get the current publication status with the following fields :

extend type JCRNode { aggregatedPublicationInfo(language: String, includesReferences: Boolean, includesSubNodes: Boolean): PublicationInfo }

extends type JCRNodeMutation {
    publish(languages: [String])
}

3.7 Vanity Urls

Vanity URLs can be queried from a node by using the vanityUrls field :
extend type JCRNode {
    vanityURLs(
    languages: [String],
    onlyActive:boolean,
    onlyDefault:boolean): [VanityURL]
}

4 Samples

4.1 Queries

{
  jcr(workspace: LIVE) {
    nodeByPath(path: "/sites/digitall/home") {
      children(typesFilter: {types: ["jnt:page"]}) {
        nodes {
          name
          name_en: displayName(language: "en")
          name_fr: displayName(language: "fr")
          createdBy: property(name: "jcr:createdBy") {
            value
          }
          descendants(limit: 2, typesFilter: {types: ["jmix:list"]}) {
            nodes {
              path
            }
          }
          ancestors(upToPath: "/sites") {
            path
          }
        }
      }
    }
  }
}

{
  jcr(workspace: LIVE) {
    nodesByQuery(query: "select * from [jnt:page]", offset: 2, limit: 10) {
      edges {
        index
        cursor
        node {
          displayName(language: "en")
        }
      }
    }
  }
}

{
  jcr(workspace: LIVE) {
    nodeByPath(path: "/sites/digitall/home") {
      children(typesFilter: {types: ["jnt:page"]}, propertiesFilter: {filters: [{property: "j:templateName", value: "home"}]}) {
        nodes {
          name
          name_en: displayName(language: "en")
          name_fr: displayName(language: "fr")
          createdBy: property(name: "jcr:createdBy") {
            value
          }
          template: property(name: "j:templateName") {
            value
          }
        }
      }
    }
  }
}

4.2 Mutations

{
  mutation {
    jcr(workspace: EDIT) {
      mutateNode(pathOrId: "/sites/mySite/home") {
        delete
      }
    }
  }
}


{
  mutation {
    jcr(workspace: EDIT) {
      addNode(parentPathOrId: "/sites/mySite", name: "page", properties: [
        {language: "en", name: "jcr:title", type: STRING, value: "Page"},
        {name: "j:templateName", type: STRING, value: "2col"}], primaryNodeType: "jnt:page") {
         uuid
      },
      modifiedNodes {
        uuid,
        name
      }
    }
  }
}

{
  mutation {
    jcr(workspace: EDIT) {
      mutateNode(pathOrId: "/sites/mySite/page") {
        mutateProperty(name: "j:templateName") {
          setValue(type: STRING, value: "3col")
        }
      }
    }
  }
}

{
  mutation {
    jcr(workspace: EDIT) {
      mutateNode(pathOrId: "/sites/mySite/page") {
        addMixins(mixins: "jmix:tagged")
      }
    }
  }
}

{
  mutation {
    jcr(workspace: EDIT) {
      mutateNode(pathOrId: "/sites/mySite/page") {
        publish
      }
    }
  }
}

4.3 GraphQL in views

A simple way to use GraphQL in DX views is to call the GraphQL endpoint with an ajax request.
You can first use GraphiQL to build your query and then use it in your view:

<script>
    var graphQLQuery =  '{  jcr {   nodesByPath(paths: "/sites/") {    children {     nodes {      path      name     }    }   }  } } '
    var xhr = new XMLHttpRequest();
    xhr.responseType = 'json';
    xhr.open("POST", "/modules/graphql");
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.setRequestHeader("Accept", "application/json");
    xhr.onload = function () {
        console.log('data returned:', xhr.response);
    }
    xhr.send(JSON.stringify({query: graphQLQuery}));
</script>
Note that you can use any framework (jQuery, etc ..) to simplify this code.

For integration with modern js frameworks (react, angular, etc ..) you can use Apollo JS client:
https://github.com/apollographql/apollo-client

5 GraphQL extensions

It is possible for a DX module to extend the GraphQL grammar, the same way as we did for the JCR implementation. The extension should provide a class implementing DXGraphQLExtensionsProvider, which will return a list of types to add to the schemas. The types are generated from plain java classes annotated with graphql-java-annotations

Example:
https://github.com/Jahia/graphql-core/tree/master/graphql-extension-example

6 GraphQL configuration

6.1 Base GraphQL configuration

Multiple configuration files can be installed in karaf/etc folder, and any module can provide its own configuration. The configuration file name should be org.jahia.modules.graphql.provider-<id>.cfg , where id is an arbitrary name identifying your configuration.
A default configuration file is available here:

/digital-factory-data/karaf/etc/org.jahia.modules.graphql.provider-default.cfg

6.1.1 CORS configuration

The configuration file contains the CORS configuration for GraphQL calls.

http.cors.allow-origin is a comma-separated list of allowed origins for CORS requests.

6.2 Packaging configuration in modules

To build your own configuration, add the configuration file in your project, prefix its name by: org.jahia.modules.graphql.provider- or org.jahia.modules.api.permissions- and edit the pom.xml of your module to reference it.
For example:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>attach-artifacts</id>
            <phase>package</phase>
            <goals>
                <goal>attach-artifact</goal>
            </goals>
            <configuration>
                <artifacts>
                    <artifact>
                        <file>
                            src/main/resources/META-INF/configurations/org.jahia.modules.graphql.provider-custom.cfg
                        </file>
                        <type>cfg</type>
                        <classifier>graphql-cfg</classifier>
                    </artifact>
                </artifacts>
            </configuration>
        </execution>
    </executions>
</plugin>

A full example is available here:
https://github.com/Jahia/graphql-core/blob/master/graphql-dxm-provider/pom.xml