Gentics Mesh - GraphQL Library Plugin Guide

Intro

In this guide you will learn how to create a plugin for Gentics Mesh which will extend the GraphQL API in order query a small library inventory.

GraphiQL Example

This example is inspired by the GraphQL Java Getting Started Guide.

All sources for this example are available on GitHub.

Requirements

Before we can start you need to install Apache Maven and a Java Development Kit.

Files

Plugin Code:

  • pom.xml

    Maven project descriptor
  • src/main/resources/schema.graphqls

    GraphQL Schema
  • src/main/java/com/gentics/mesh/plugin/GraphQLDataFetchers.java

    Data fetchers for the inventory data
  • src/main/java/com/gentics/mesh/plugin/GraphQLExamplePlugin.java

    The actual plugin implementation

Test Code:

  • src/test/java/com/gentics/mesh/plugin/GraphQlExamplePluginTest.java

    Plugin integration test
  • src/test/java/com/gentics/mesh/plugin/PluginRunnerExample.java

    Example runner which will start Mesh and the plugin

Project Setup

/pom.xml

The first thing we need to do is to setup our project. In this example we’ll use the pom.xml file which is used by Apache Maven to build the plugin project.

The pom.xml will also contain plugin metadata. It is the place where the plugin manifest will be specified that is later added to the final .jar file.

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.gentics.mesh.plugin</groupId>
  <artifactId>mesh-graphql-library-example-plugin</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <mesh.version>0.38.0</mesh.version>

    <!-- We use these properties to specify the plugin manifest -->
    <plugin.id>library</plugin.id>
    <plugin.name>The GraphQL example plugin</plugin.name>
    <plugin.description>A very simple plugin which shows how to extend the
      GraphQL API.</plugin.description>
    <plugin.class>com.gentics.mesh.plugin.GraphQLExamplePlugin</plugin.class>
    <plugin.version>${project.version}</plugin.version>
    <plugin.license>Apache License 2.0</plugin.license>
    <plugin.author>Joe Doe</plugin.author>
    <plugin.inception>2019-07-08</plugin.inception>
  </properties>

  <!-- The dependency management section provides the versions for the needed artifacts -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.gentics.mesh</groupId>
        <artifactId>mesh-plugin-bom</artifactId>
        <version>${mesh.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <!-- The mesh plugin api dependencies.
         Please note that these dependencies need to be set to provided -->
    <dependency>
      <groupId>com.gentics.mesh</groupId>
      <artifactId>mesh-plugin-dep</artifactId>
      <scope>provided</scope>
    </dependency>

    <!-- Test dependencies -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.gentics.mesh</groupId>
      <artifactId>mesh-test-common</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.gentics.mesh</groupId>
      <artifactId>mesh-core</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
          <verbose>true</verbose>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
      <!--The shade plugin will generate the jar with all the needed dependencies -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <!-- We use the transformer to add the manifest properties to the jar manifest -->
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <manifestEntries>
                    <Plugin-Id>${plugin.id}</Plugin-Id>
                    <Plugin-Version>${plugin.version}</Plugin-Version>
                    <Plugin-Author>${plugin.author}</Plugin-Author>
                    <Plugin-Class>${plugin.class}</Plugin-Class>
                    <Plugin-Description>${plugin.description}</Plugin-Description>
                    <Plugin-License>${plugin.license}</Plugin-License>
                    <Plugin-Inception>${plugin.inception}</Plugin-Inception>
                  </manifestEntries>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <!-- The repository section is important.
       Otherwise you will not be able to download the artifacts -->
  <repositories>
    <repository>
      <id>maven.gentics.com</id>
      <name>Gentics Maven Repository</name>
      <url>https://maven.gentics.com/maven2</url>
      <releases>
        <enabled>true</enabled>
      </releases>
    </repository>
  </repositories>

</project>

Schema

The schema.graphqls file contains the GraphQL Schema definition (SDL) which is used to setup the types for the inventory. It defines fields for each type. In our case we only need the Book and the Author types.

type Query {
  bookById(id: ID): Book
}

type Book {
  id: ID
  name: String
  pageCount: Int
  author: Author
}

type Author {
  id: ID
  firstName: String
  lastName: String
}

Plugin Code

GraphQLExamplePlugin.java

The GraphQLExamplePlugin file contains the actual plugin code. The plugin itself is very simple.

We need to extend the AbstractPlugin class and implement the GraphQLPlugin interface since our plugin will extend the GraphQL API.

In the initialize() method we setup the GraphQLSchema for our plugin. The counterpart is the shutdown() method.

package com.gentics.mesh.plugin;

import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;

import java.net.URL;

import org.pf4j.PluginWrapper;

import com.gentics.mesh.plugin.env.PluginEnvironment;
import com.gentics.mesh.plugin.graphql.GraphQLPlugin;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;

import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.reactivex.Completable;

public class GraphQLExamplePlugin extends AbstractPlugin implements GraphQLPlugin {

	private GraphQLSchema schema;

	private GraphQLDataFetchers fetcher = new GraphQLDataFetchers();

	public GraphQLExamplePlugin(PluginWrapper wrapper, PluginEnvironment env) {
		super(wrapper, env);
	}

	@Override
	public Completable initialize() {
		return Completable.fromAction(() -> {
			URL url = Resources.getResource("schema.graphqls");
			String sdl = Resources.toString(url, Charsets.UTF_8);
			schema = buildSchema(sdl);
		});
	}

	private GraphQLSchema buildSchema(String sdl) {
		TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
		RuntimeWiring runtimeWiring = buildWiring();
		SchemaGenerator schemaGenerator = new SchemaGenerator();
		return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
	}

	private RuntimeWiring buildWiring() {
		return RuntimeWiring.newRuntimeWiring()
			.type(newTypeWiring("Query")
				.dataFetcher("bookById", fetcher.getBookByIdDataFetcher()))
			.type(newTypeWiring("Book")
				.dataFetcher("author", fetcher.getAuthorDataFetcher()))
			.build();
	}

	@Override
	public GraphQLSchema createRootSchema() {
		return schema;
	}

}

Data Fetcher

The data fetcher contains the code that is used to actually retrieve the inventory information. One fetcher is used for looking up books by id. The other one will return the author.

package com.gentics.mesh.plugin;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import com.google.common.collect.ImmutableMap;

import graphql.schema.DataFetcher;

public class GraphQLDataFetchers {

	private static List<Map<String, String>> books = Arrays.asList(
		ImmutableMap.of("id", "book-1",
			"name", "Harry Potter and the Philosopher's Stone",
			"pageCount", "223",
			"authorId", "author-1"),
		ImmutableMap.of("id", "book-2",
			"name", "Moby Dick",
			"pageCount", "635",
			"authorId", "author-2"),
		ImmutableMap.of("id", "book-3",
			"name", "Interview with the vampire",
			"pageCount", "371",
			"authorId", "author-3"));

	private static List<Map<String, String>> authors = Arrays.asList(
		ImmutableMap.of("id", "author-1",
			"firstName", "Joanne",
			"lastName", "Rowling"),
		ImmutableMap.of("id", "author-2",
			"firstName", "Herman",
			"lastName", "Melville"),
		ImmutableMap.of("id", "author-3",
			"firstName", "Anne",
			"lastName", "Rice"));

	public DataFetcher getBookByIdDataFetcher() {
		return dataFetchingEnvironment -> {
			String bookId = dataFetchingEnvironment.getArgument("id");
			return books
				.stream()
				.filter(book -> book.get("id").equals(bookId))
				.findFirst()
				.orElse(null);
		};
	}

	public DataFetcher getAuthorDataFetcher() {
		return dataFetchingEnvironment -> {
			Map<String, String> book = dataFetchingEnvironment.getSource();
			String authorId = book.get("authorId");
			return authors
				.stream()
				.filter(author -> author.get("id").equals(authorId))
				.findFirst()
				.orElse(null);
		};
	}
}

Testing

For testing it is possible to use the MeshLocalServer class rule which will make it possible to start-up an in-memory Gentics Mesh server that can be used to run your plugin.

The test will:

  • Start Gentics Mesh in memory

  • Deploy the plugin

  • Setup an empty project

  • Finally run the the query to fetch the data from the plugin

package com.gentics.mesh.plugin;

import java.io.IOException;

import org.junit.ClassRule;
import org.junit.Test;

import com.gentics.mesh.core.rest.graphql.GraphQLResponse;
import com.gentics.mesh.core.rest.project.ProjectCreateRequest;
import com.gentics.mesh.rest.client.MeshRestClient;
import com.gentics.mesh.test.local.MeshLocalServer;

import io.reactivex.Completable;
import io.reactivex.Single;

/**
 * This example test demonstrates how to use the {@link MeshLocalServer} to deploy a plugin.
 */
public class GraphQlExamplePluginTest {

	private static String PROJECT = "test";

	@ClassRule
	public static final MeshLocalServer server = new MeshLocalServer()
		.withInMemoryMode()
		.withPlugin(GraphQLExamplePlugin.class, "myPlugin")
		.waitForStartup();

	@Test
	public void testPlugin() throws IOException {
		MeshRestClient client = server.client();

		// Create the project
		Completable createProject = client.createProject(
			new ProjectCreateRequest()
				.setName(PROJECT)
				.setSchemaRef("folder"))
			.toCompletable();

		// Run query on the plugin
		String query = "{ pluginApi { myPlugin { text } } }";
		Single<GraphQLResponse> rxQuery = client.graphqlQuery(PROJECT, query)
			.toSingle();

		// Run the operations
		GraphQLResponse result = createProject.andThen(rxQuery).blockingGet();

		// Print the GraphQL API result
		System.out.println(result.getData().encodePrettily());
	}
}

When running the test you should see the output in the result of the GraphQL test request.

INFO: 127.0.0.1 - POST /api/v1/test/graphql HTTP/1.1 200 106 - 12 ms
{
  "pluginApi" : {
    "myPlugin" : {
      "bookById" : {
        "id" : "book-1",
        "name" : "Harry Potter and the Philosopher's Stone"
      }
    }
  }
}

Packaging

Now we can package the plugin by running mvn clean package. The built plugin will be placed in the target directory.

We can now place the mesh-graphql-library-example-plugin-0.0.1-SNAPSHOT.jar file in the Gentics Mesh plugins directory.

Deployment

You can now deploy start a new Gentics Mesh instance which will deploy the plugin during start-up:

docker run --rm \
    -v $PWD/target/mesh-graphql-library-example-plugin-0.0.1-SNAPSHOT.jar:/plugins/graphql.jar \
    -p 8080:8080 \
    gentics/mesh:0.38.0

It is also possible to deploy a plugin in a running instance via the /admin/plugins endpoint.

With an admin token at hand you can invoke the deployment via cURL:

curl -X POST \
 -d '{
    "path":"mesh-graphql-library-example-plugin-0.0.1-SNAPSHOT.jar"
    }' \
 -H "Authorization: Bearer $MESH_TOKEN" \
 -H "Content-Type: application/json" \
 $MESH_BASE/admin/plugins

You can read more about how to use the REST API with cURL in the API guide.

Accessing

Once the plugin has been deployed you can use the plugin via the GraphQL endpoint.

curl -X POST \
 -d '{
    "query":"{ pluginApi { myPlugin { bookById(id: \"book-1\") {id name } } } }"
    }' \
 -H "Content-Type: application/json" \
 $MESH_BASE/test/graphql