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.
DX comes with several GraphQL OSGi bundles.
https://github.com/Jahia/graphql-java
This is the Java implementation of GraphQL specifications
https://github.com/Jahia/graphql-java-servlet
This bundle provides the following GraphQL endpoint:
/{context}/modules/graphql
https://github.com/Jahia/graphql-java-annotations
This bundle provides annotations for GraphQL integration.
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
We provide a jcr implementation that allows to perform queries and mutations against the repository.
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).
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.
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")
}
}
}
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.
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.
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.
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 :
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.
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.
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 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 :
addNode
field. This returns a JCRNodeMutation
object, on which subsequent operations can be done on the added node.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.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.
mutateProperty
and mutateProperties
fields, which return a JCRPropertyMutation
object (or a list of JCRPropertyMutation
objects)mutateDescendant
, mutateChildren
and mutateDescendants
fields, which return a JCRNodeMutation
object on the sub node.addChild
, similar to the addNode
at JCRMutation
level, without specifying the parentaddChildrenBatch
, like the addNodesBatch
at JCRMutation
level.addMixins
/ removeMixins
fieldsmove
/ rename
fieldsdelete
/ markForDeletion
/ unmarkForDeletion
fieldsreorderChildren
field, based on a list of names. Children not listed in parameter are ignored in the reordering.setPropertiesBatch
. If properties were already existing, the value is entirely replaced with the one passed in parameterNote 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.
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.
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
}
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")
extend type JCRNode {
lockInfo: LockInfo
}
extend type JCRNodeMutation {
lock(type: String, recursive: Boolean): Boolean
unlock(type: String, recursive: Boolean force: Boolean): Boolean
}
extend type JCRNode { aggregatedPublicationInfo(language: String, includesReferences: Boolean, includesSubNodes: Boolean): PublicationInfo }
extends type JCRNodeMutation {
publish(languages: [String])
}
vanityUrls
field :
extend type JCRNode {
vanityURLs(
languages: [String],
onlyActive:boolean,
onlyDefault:boolean): [VanityURL]
}
{
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
}
}
}
}
}
}
{
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
}
}
}
}
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>
For integration with modern js frameworks (react, angular, etc ..) you can use Apollo JS client:
https://github.com/apollographql/apollo-client
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: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
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.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