GraphQL

Explicit schemas

personClever Llamas
calendar_today2023-02-02

In the previous article, GraphQL - Getting started, we looked at what drives the GraphQL service provided by MarkLogic 11 and how various basic options are readily available out of the box. With this article we're taking things a little further by creating an explicit schema with some more complex type and query definitions for our data.

Setup

Before we begin we'll setup our system with the base TDE templates and the llamaverse loaded. For this article we'll use the Documents database for our content and the Schemas database for the TDE template. For simplicity all samples are executed as admin.

'use strict';
declareUpdate();

var tde = require("/MarkLogic/tde.xqy");
var template = {
    template: {
        context: "/features[fn:lower-case(./properties/type) = 'llama']",
        collections: [{ collectionsAnd: ["/type/llamaverse"] }],
        rows: [
            {
                schemaName: "llamaverse",
                viewName: "llamas",
                columns: [
                    {
                        name: "id",
                        scalarType: "int",
                        val: "./properties/id",
                    },
                    {
                        name: "name",
                        scalarType: "string",
                        val: "./properties/name",
                    },
                    {
                        name: "type",
                        scalarType: "string",
                        val: "./properties/type",
                    },
                    {
                        name: "birthDate",
                        scalarType: "date",
                        val: "./properties/birthDate",
                    },
                    {
                        name: "pastureId",
                        scalarType: "int",
                        val: "./properties/pasture",
                    },
                ],
            },
        ],
    },
};
tde.templateInsert("/llamaverse/llamas.json", template);
'use strict';
declareUpdate();

var tde = require("/MarkLogic/tde.xqy");
var template = {
    template: {
        context: "/features[fn:lower-case(./properties/type) = 'pasture']",
        collections: [{ collectionsAnd: ["/type/llamaverse"] }],
        rows: [
            {
                schemaName: "llamaverse",
                viewName: "pastures",
                columns: [
                    {
                        name: "id",
                        scalarType: "int",
                        val: "./properties/id",
                    },
                    {
                        name: "name",
                        scalarType: "string",
                        val: "./properties/name",
                    },
                    {
                        name: "type",
                        scalarType: "string",
                        val: "./properties/type",
                    },
                    {
                        name: "farmId",
                        scalarType: "int",
                        val: "./properties/farm",
                    },
                ],
            },
        ],
    },
};
tde.templateInsert("/llamaverse/pastures.json", template);
'use strict';
declareUpdate();

var tde = require("/MarkLogic/tde.xqy");
var template = {
    template: {
        context: "/features[fn:lower-case(./properties/type) = 'farm']",
        collections: [{ collectionsAnd: ["/type/llamaverse"] }],
        rows: [
            {
                schemaName: "llamaverse",
                viewName: "farms",
                columns: [
                    {
                        name: "id",
                        scalarType: "int",
                        val: "./properties/id",
                    },
                    {
                        name: "name",
                        scalarType: "string",
                        val: "./properties/name",
                    },
                    {
                        name: "type",
                        scalarType: "string",
                        val: "./properties/type",
                    },
                ],
            },
        ],
    },
};
tde.templateInsert("/llamaverse/farms.json", template);
'use strict';
declareUpdate();

const llamaverse = {
    creator: "CleverLlamas",
    name: "Llamaverse",
    features: [
        {
            type: "Feature",
            properties: {
                id: 2,
                name: "High Hill Field",
                type: "pasture",
                farm: 1,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 3,
                name: "Blackwater Pasture",
                type: "pasture",
                farm: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 1,
                name: "Valley Pasture",
                type: "pasture",
                farm: 3,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 4,
                name: "Northern Fields",
                type: "pasture",
                farm: 3,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 1,
                name: "High Hill Farm",
                type: "farm",
            },
        },
        {
            type: "Feature",
            properties: {
                id: 2,
                name: "Blackwater Ranch",
                type: "farm",
            },
        },
        {
            type: "Feature",
            properties: {
                id: 3,
                name: "Llama Valley Estate",
                type: "farm",
            },
        },
        {
            type: "Feature",
            properties: {
                id: 1,
                name: "George",
                type: "llama",
                birthDate: "2007-06-01",
                farm: 3,
                pasture: 4,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 2,
                name: "Charlie",
                type: "llama",
                birthDate: "2002-06-10",
                farm: 3,
                pasture: 4,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 3,
                name: "Winnie",
                type: "llama",
                birthDate: "1998-11-22",
                farm: 3,
                pasture: 4,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 4,
                name: "Loretta",
                type: "llama",
                birthDate: "1996-11-14",
                farm: 3,
                pasture: 4,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 5,
                name: "Pablo",
                type: "llama",
                birthDate: "2003-11-11",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 6,
                name: "Maria",
                type: "llama",
                birthDate: "2003-05-26",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 7,
                name: "Jeff",
                type: "llama",
                birthDate: "2003-05-11",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 8,
                name: "Tiff",
                type: "llama",
                birthDate: "2002-03-20",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 9,
                name: "Larry",
                type: "llama",
                birthDate: "2005-10-02",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 10,
                name: "Duke",
                type: "llama",
                birthDate: "2005-09-28",
                farm: 2,
                pasture: 3,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 11,
                name: "Elle",
                type: "llama",
                birthDate: "2005-03-24",
                farm: 2,
                pasture: 3,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 12,
                name: "Chip",
                type: "llama",
                birthDate: "1999-04-12",
                farm: 2,
                pasture: 3,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 13,
                name: "Beth",
                type: "llama",
                birthDate: "1999-10-19",
                farm: 2,
                pasture: 3,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 14,
                name: "Ruby",
                type: "llama",
                birthDate: "2000-06-04",
                farm: 2,
                pasture: 3,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 15,
                name: "James",
                type: "llama",
                birthDate: "2000-07-01",
                farm: 3,
                pasture: 1,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 16,
                name: "William",
                type: "llama",
                birthDate: "2002-08-15",
                farm: 3,
                pasture: 1,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 17,
                name: "Haley",
                type: "llama",
                birthDate: "2003-06-22",
                farm: 3,
                pasture: 4,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 18,
                name: "Ben",
                type: "llama",
                birthDate: "2004-01-22",
                farm: 3,
                pasture: 4,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 19,
                name: "Adam",
                type: "llama",
                birthDate: "2004-12-10",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 20,
                name: "Jenny",
                type: "llama",
                birthDate: "2000-08-11",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 21,
                name: "Mick",
                type: "llama",
                birthDate: "2005-02-06",
                farm: 1,
                pasture: 2,
            },
        },
        {
            type: "Feature",
            properties: {
                id: 22,
                name: "Lizzy",
                type: "llama",
                birthDate: "2005-03-22",
                farm: 1,
                pasture: 2,
            },
        },
    ],
};
xdmp.documentInsert("/llamaverse.json", llamaverse, {
    collections: ["/type/llamaverse"],
});

Llamaverse

Consider what the map of our llamaverse would look like for a moment and look beyond some the basic aspects such as its boundary, the roads, and the water...

It also holds our three entities; llamas, pastures, and farms. When we defined each of these entities within a TDE template they have become automatically available as GraphQL types within the implicit schema that was generated for us by MarkLogic 11. But, looking at the map and some of the properties in our data, they are obviously related to eachother; a llama is located within a pasture and a pasture is associated with a farm. Yet within the implicit schema this hierarchy is not expressed which makes writing of more complex queries impossible.

Explict schema

In order to fix this issue the relationship between each entity needs to be expressed using some of MarkLogic's custom directives. Remember the implicit schema the we reviewed in the first article:

type llamaverse_llamas @View(schemaName: "llamaverse", viewName: "llamas") {
  id: Int
  name: String
  type: String
  birthDate: String
  pastureId: Int
}

type llamaverse_pastures @View(schemaName: "llamaverse", viewName: "pastures") {
  id: Int
  name: String
  type: String
  farmId: Int
}

type llamaverse_farms @View(schemaName: "llamaverse", viewName: "farms") {
  id: Int
  name: String
  type: String
}

type Query {
  llamaverse_llamas(id: Int, name: String, type: String, birthDate: String, pastureId: Int): [llamaverse_llamas]
  llamaverse_pastures(id: Int, name: String, type: String, farmId: Int): [llamaverse_pastures]
  llamaverse_farms(id: Int, name: String, type: String): [llamaverse_farms]
}

@View directive

Each type is based on a view by using the custom @View directive to specify the schema- and viewname of the TDE template that our GraphQL type definition is mapped to. Each field defined in a type is mapped to its corresponding column in the view.

BEWARE

A scalar type field must match a TDE template column by name. Querying a field that does not have a corresponding column definition in the TDE template will result in a XDMP-ARGTYPE: addNode(null) -- arg1 is not of type Node error.

@Join directive

The custom @Join directive specifies a leftJoinColumn and rightJoinColumn and can be used to express relationships between our type definitions (or basically their respective views) by indicating which columns from the views are used for the join. In our case each view already includes a column that represents a foreign key.

Types

For this article we'll define two new types, one similar to our existing 'llamaverse_farms' type that will be called 'custom_farms' and another similar to our existing 'llamaverse_pastures' type that we will call 'custom_pastures'.
Both types will define which of our TDE's columns will be exposed as fields and on each type we will define an additional list type field that represents a list of items. For example, on our 'custom_farms' type this field will be called 'pastures' which represents an item list of type 'custom_pasture'. Do note that tmplicit and explicit definitions can be mixed. Our explicit 'custom_pastures' definition references the implicit 'llamaverse_llamas' definition.

NOTE

Although a list type field definition that shadows the name of an existing TDE template column does not cause any errors we highly advise against this. For example, imagine our TDE template already includes a column 'pastures' that holds a count of the number of pastures for the farm. In that case 'pasturesList' would have been a better name for our list type field.

Queryies

For these new type definitions to be used in a GraphQL query we will need to define two additional fields on the Query root operation type. In our case we have defined these fields with the same name as their type, which follows the same pattern as the automatically generated implicit schema. Both queries are configured to return a list of their respective type.

NOTE

Where the queries within the implicit schema include the field definitions of their type this is not required nor does it have any effect on the execution of the GraphQL query. For that reason we've chosen not to follow the same pattern as it only causes confusion when type definition and the Query field definition become out of sync.

Configuration

GraphQL can use the newly defined types and queries once we have stored and configured them as an explicit schema. The schema has to be added to the schemas database of the database for which you will query the content. In our case this will be the Schemas database.

'use strict';
declareUpdate();

let sdlString = `
type custom_farms @View(schemaName: "llamaverse", viewName: "farms") {
    id: Int
    name: String
    type: String
    pastures: [custom_pastures] @Join(leftJoinColumn: "id", rightJoinColumn: "farmId")
}

type custom_pastures @View(schemaName: "llamaverse", viewName: "pastures") {
    id: Int
    name: String
    type: String
    llamas: [llamaverse_llamas] @Join(leftJoinColumn: "id", rightJoinColumn: "pastureId")
}

type Query {
    custom_farms: [custom_farms]
    custom_pastures: [custom_pastures]
}
`;
let textNodeExplicit = new NodeBuilder();
textNodeExplicit.addText(sdlString);
xdmp.documentInsert("/graphql/explicitSchema.sdl", textNodeExplicit.toNode());

With the explicit schema in place we'll have to configure the GraphQL service to use it. For this a simple JSON configuration will need to be inserted into the same schemas database. This configuration file points to the schema file that holds our explicit schema.

'use strict';
declareUpdate();

var configDoc = {
    "schemaUri":"/graphql/explicitSchema.sdl"
};
xdmp.documentInsert("/graphql/config.json", configDoc);

Query

Now we're finally ready to query the GraphQL service using our newly defined definitions. The query example below shows how we query our 'custom_farms' while we pass an argument 3 to its field 'id'. As we traverse its now related objects we request the 'name' field for each. The result shows the farm, the names of its pastures, and their llamas are now returned in a single request.

query {
    custom_farms (id: 3) {
        name
        pastures {
            name
            llamas {
                name
            }
        }
    }
}
{
    "data": {
        "custom_farms": [
            {
                "name": "Llama Valley Estate",
                "pastures": [
                    {
                        "name": "Valley Pasture",
                        "llamas": [
                            {
                                "name": "James"
                            },
                            {
                                "name": "William"
                            }
                        ]
                    },
                    {
                        "name": "Northern Fields",
                        "llamas": [
                            {
                                "name": "Loretta"
                            },
                            {
                                "name": "Charlie"
                            },
                            {
                                "name": "George"
                            },
                            {
                                "name": "Haley"
                            },
                            {
                                "name": "Winnie"
                            },
                            {
                                "name": "Ben"
                            }
                        ]
                    }
                ]
            }
        ]
    },
    "errors": []
}

Conclusion

Once you start writing explicit schemas GraphQL really becomes a useful addition to the list of services available to view your data. There are still some limitations though, as currently only available custom directives can be used and you're not able to write your own. And although the implicit schema is available "out-of-the-box", many of us will find that in pratice writing explicit schemas will be required quickly.

Overall we're excited about the feature and sure hope to see it extended in future releases to allow developers to create rich GraphQL endpoints.

Need Some Help?


Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!