Embedding

Contents

Guillotine is also available as a standalone library which may be embedded into your custom Enonic application. This gives you extensive control over the API.

To create a new app with the Guillotine library boot-strapped: enonic project create -r starter-guillotine

Follow the instructions below to embed Guillotine into your existing application.

Add library

Update the dependencies of your project:

/build.gradle
dependencies {
    implementation "com.enonic.xp:portal-api:${xpVersion}"
    include "com.enonic.lib:lib-guillotine:<version>"
}
Replace <version> with the actual version you want to use, see available versions on Enonic Market

Creating an endpoint

When using the Guillotine library, you control where and how your API is exposed. Most commonly, the API is mounted in the context of a site, which means you will have to add a site.xml file in your app, and then add the application to the specific site:

Controller

Starting off, you will need a minimal JavaScript controller file that will handle the requests. Below is a minimal example:

Sample JS controller
var guillotineLib = require('/lib/guillotine'); (1)
var graphQlLib = require('/lib/graphql'); (1)

var schema = guillotineLib.createSchema(); (2)

exports.post = function (req) { (3)
 var body = JSON.parse(req.body); (4)
 var result = JSON.stringify(graphQlLib.execute(schema, body.query, body.variables)); (5)
 return {
     contentType: 'application/json',
     body: JSON.stringify(result)
 };
};
1 The GraphQL library is already a dependency in Guillotine and does not need explicitly need to be added to your Gradle file
2 Creates the GraphQL schema the first time the service is called.
3 Handles POST requests
4 Parses the JSON body to retrieve the GraphQL query and variables
5 Executes the query and variables against the schema created

Mounting

The controller alone is not enough to give you an end-point. The following approaches are available.

The various approaches are described below:

Service

Service endpoints are automatically mounted in a unique URL-pattern, contextual of your site.

Simply place the controller file above in your app using the following pattern: /src/main/resources/services/<service-name>/<service-name>.js

That’s it

Mapping

As opposed to services, mappings are explicitly declared.

This time, however, you may place the controller anywhere within your app resources structure, i.e. /src/main/resources/controllers/guillotine.js. Then declare the mapping in your site.xml schema. For instance like this

/resources/site/site.xml
<mappings>
  <mapping controller="/controllers/graphql.js" order="50">
    <pattern>/myapi</pattern>
  </mapping>
</mappings>

Using the example above, the API will be available directly in context of your site like this: <url-to-site>/myapi.

Filters

Filters are similar to mappings, but may also work in combination with other controller on the same path. I.e. a filter handling POST requests may for instance be mapped to the site root, even if your site serves content using GET requests on the same endpoint.

Example filter mapping

/resources/site/site.xml
<mappings>
  <mapping filter="/controllers/graphql.js" order="50">
    <pattern>/</pattern>
  </mapping>
</mappings>

Subscriptions and websockets

The Guillotine API also support GraphQL subscriptions. This is implemented via websockets. To enable this functionality, your controller must be extended.

Make sure you have a dependency for websockets in your application:

build.gradle with websocket dependency
dependencies {
    implementation "com.enonic.xp:portal-api:${xpVersion}"
    include "com.enonic.xp:lib-websocket:${xpVersion}"
    include "com.enonic.lib:lib-guillotine:<version>"
}
Controller with Websockets
const guillotineLib = require('/lib/guillotine');

exports.get = function (req) {
    if (req.webSocket) {
        return {
            webSocket: {
                data: guillotineLib.createWebSocketData(req),
                subProtocols: ['graphql-ws']
            }
        };
    }
};

exports.post = function (req) {
    let input = JSON.parse(req.body);

    let params = {
        query: input.query,
        variables: input.variables
    };

    return {
        contentType: 'application/json',
        body: guillotineLib.execute(params)
    };
};

exports.webSocketEvent = guillotineLib.initWebSockets();

CORS headers

To enable flexible access to the API from browser clients, you may also need to set specific CORS headers.

The example below includes both support for websockets and CORS headers:

Controller with CORS headers
const guillotineLib = require('/lib/guillotine');

//──────────────────────────────────────────────────────────────────────────────
// Constants
//──────────────────────────────────────────────────────────────────────────────
const CORS_HEADERS = {
    'Access-Control-Allow-Headers': 'Content-Type',
    'Access-Control-Allow-Methods': 'POST, OPTIONS',
    'Access-Control-Allow-Origin': '*'
};

//──────────────────────────────────────────────────────────────────────────────
// Methods
//──────────────────────────────────────────────────────────────────────────────
exports.options = function () {
    return {
        contentType: 'text/plain;charset=utf-8',
        headers: CORS_HEADERS
    };
};

exports.get = function (req) {
    if (req.webSocket) {
        return {
            webSocket: {
                data: guillotineLib.createWebSocketData(req),
                subProtocols: ['graphql-ws']
            }
        };
    }
};

exports.post = function (req) {
    let input = JSON.parse(req.body);

    let params = {
        query: input.query,
        variables: input.variables
    };

    return {
        contentType: 'application/json',
        headers: CORS_HEADERS,
        body: guillotineLib.execute(params)
    };
};

exports.webSocketEvent = guillotineLib.initWebSockets();

API browser

Finally - to simplify use of your API, you may also embed a GraphQL API browser on the very same endpoint. As oppsed to the GraphQL API, which uses the POST method, the API browser will be handling the GET requests.

Make sure you have a dependency to GraphQL playground

build.gradle with all dependencies
dependencies {
    implementation "com.enonic.xp:portal-api:${xpVersion}"
    include "com.enonic.xp:lib-websocket:${xpVersion}"
    include "com.enonic.lib:lib-guillotine:<version>"
    include "com.enonic.lib:lib-graphql-playground:<version>"
}
JS controller with all features
const guillotineLib = require('/lib/guillotine');
const graphqlPlaygroundLib = require('/lib/graphql-playground');

//──────────────────────────────────────────────────────────────────────────────
// Constants
//──────────────────────────────────────────────────────────────────────────────
const CORS_HEADERS = {
    'Access-Control-Allow-Headers': 'Content-Type',
    'Access-Control-Allow-Methods': 'POST, OPTIONS',
    'Access-Control-Allow-Origin': '*'
};

//──────────────────────────────────────────────────────────────────────────────
// Methods
//──────────────────────────────────────────────────────────────────────────────
exports.options = function () {
    return {
        contentType: 'text/plain;charset=utf-8',
        headers: CORS_HEADERS
    };
};

exports.get = function (req) {
    if (req.webSocket) {
        return {
            webSocket: {
                data: guillotineLib.createWebSocketData(req),
                subProtocols: ['graphql-ws']
            }
        };
    }

    let body = graphqlPlaygroundLib.render();
    return {
        contentType: 'text/html; charset=utf-8',
        body: body
    };
};

exports.post = function (req) {
    let input = JSON.parse(req.body);

    let params = {
        query: input.query,
        variables: input.variables
    };

    return {
        contentType: 'application/json',
        headers: CORS_HEADERS,
        body: guillotineLib.execute(params)
    };
};

exports.webSocketEvent = guillotineLib.initWebSockets();

Contents

Contents