Using GraphQL to perform queries
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 Jahia.
Please consult http://graphql.org/learn/ to understand GraphQL concepts.
OSGi bundles
Jahia comes with several GraphQL OSGi bundles.
Graphql-java
https://github.com/Jahia/graphql-java
This is the Java implementation of GraphQL specifications
Graphql-java-servlet
https://github.com/Jahia/graphql-java-servlet
This bundle provides the following GraphQL endpoint:
/{context}/modules/graphql
Graphql-java.annotations
https://github.com/Jahia/graphql-java-annotations
This bundle provides annotations for GraphQL integration.
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
GraphQL JCR Implementation
We provide a jcr implementation that allows to perform queries and mutations against the repository.
Generalities on Jahia GraphQL Schema
Session
The session (logged user) used for queries is the JCR Session from the current HTTP request. If you not logged in into Jahia, 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 Jahia, 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).
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.
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")
}
}
}
Authentication and authorization
GraphQL uses HTTP and relies on standard Jahia 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.
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.
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.
Generic JCR nodes representation
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.
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.
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
}
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 aJCRNodeMutation
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 usingaddNode
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
, andmutateNodesByQuery
fields. These mutations fields returns aJCRNodeMutation
object (or a list ofJCRNodeMutation
). delete
/markForDeletion
/unmarkForDeletion
withdeleteNode
,markNodeForDeletion
andunmarkNodeForDeletion
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
andmutateProperties
fields, which return aJCRPropertyMutation
object (or a list ofJCRPropertyMutation
objects) - Edit descendants with
mutateDescendant
,mutateChildren
andmutateDescendants
fields, which return aJCRNodeMutation
object on the sub node. - Add children with
addChild
, similar to theaddNode
atJCRMutation
level, without specifying the parent - Add children in batch can also be done with
addChildrenBatch
, like theaddNodesBatch
atJCRMutation
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 toJCRMutation.mutateNode.addChild
deleteNode
is equivalent toJCRMutation.mutateNode.delete
undeleteNode
is equivalent toJCRMutation.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.
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.
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")
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
}
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])
}
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]
}
Samples
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
}
}
}
}
}
}
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
}
}
}
}
GraphQL in views
A simple way to use GraphQL in Jahia 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>
For integration with modern js frameworks (react, angular, etc ..) you can use Apollo JS client:
https://github.com/apollographql/apollo-client
GraphQL extensions
It is possible for a Jahia 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
GraphQL configuration
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
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.
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