GraphQL API

Overview

The GraphQL plugin works side by side with the already existing REST endpoints to get a managed unified GraphQL API to build modern applications.

The restheart-graphql service added to RESTHeart exposes a read-only (no mutations and subscription) GraphQL API for inquiring MongoDB resources.

Configuration

For each GraphQL application you need to define a so called GraphQL app definition. This is a JSON document to create in a MongoDB collection. You can specify the collection that holds the app definitions in the RESTHeart’s configuration as follow:

plugins-args:
  graphql:
    enabled: true
    secured: false
    uri: /graphql
    db: <db_name>
    collection: <reserved_collection_name>
    verbose: false

by default:

  • <db_name>= restheart

  • <reserved_collection_name>= gql-apps

This kind of configuration allows you to change dynamically the behavior of your GraphQL application by updating the related document on MongoDB.

GraphQL App Definition

A GraphQL application definition is composed by three sections:

{
  "descriptor": "...",
  "schema": "...",
  "mappings": "..."
}

Descriptor

Here you can specify:

  • name: GraphQL application name.

  • description: GraphQL application description.

  • enabled: can be true or false. If it’s false, the GraphQL application can’t be queried and vice-versa. By default it is true`.

  • uri: it specifies at which endpoint your GraphQL application is reachable (e.g. /graphql/uri). If you don’t specify the URI, application’s name is used instead (so, at least one of name or URI must be present).

{
  "descriptor": {
    "name": "MyApp",
    "description": "my first test GraphQL application",
    "enabled": true,
    "uri": "myapp"
  },
  "schema": "...",
  "mappings": "..."
}

Schema

This section must contain GraphQL application’s schema written with Schema Definition Language (SDL). For example:

{
  "descriptor": "...",
  "schema": "type User{name: String surname: String email: String posts: [Post]} type Post{text: String author: User} type Query{users(limit: Int = 0, skip: Int = 0)}",
  "mappings": "..."
}
Note
In order the schema to be a valid, it must contain the type Query.

Mappings

In this section you can specify how GraphQL types fields are mapped on MongoDB data. Mappings have to be organized following the same hierarchical structure of GraphQL SDL schema, so for each GraphQL type you can insert a JSON object with a property for each field that you want to map.

Two kinds of mapping can be made:

  • Field to Field mapping

  • Field to Query mapping

  • Field to Aggregation mapping (*)

Note
(*) the new Field to Aggregation mapping is only available on snapshot release and foreseen for future releases.

Field to Field mapping

You can map a GraphQL field with a specific MongoDB document field or with an element of a MongoDB array by dot-notation. For instance:

{
  "descriptor": "...",
  "schema": "...",
  "mappings":{
    "User": {
      "name": "firstName",
      "phone": "contacts.phone",
      "email": "contacts.emails.0",
    },
    "Post": "...",
    "Query": "..."
  }
}

Whit this configuration:

  • name is mapped with MongoDB document firstName field

  • phone is mapped with field phone in contacts nested document

  • email is mapped with 1st element of emails array within contacts nested document

Notice that, if you don’t specify a mapping for a field, RESTHeart will map it with a MongoDB document field with the same name.

Field to Query mapping

You can map a GraphQL field with a MongoDB query using the following parameters:

  • db (String): database name;

  • collection (String): collection name;

  • find (Document): selection filter using query operators (e.g. $in, $and, $or, …​);

  • sort (Document): order in which the query returns matching documents;

  • skip (Document or Integer): how many documents should be skipped of those resulting;

  • limit (Document or Integer): how many documents should be returned at most of those resulting.

Note
Starting from v6.5.1, unlimited queries are not allowed: if the query does not specifies a limit, the service configuration default-limit is applied. Also the limit cannot exceed the max-limit. The default GraphQL service configuration in restheart.yml follows:
plugins-args:
  graphql:
    uri: /graphql
    db: restheart
    collection: gql-apps
    # default-limit is used for queries that don't not specify a limit
    default-limit: 100
    # max-limit is the maximum value for a Query limit
    max-limit: 1000
    verbose: false

Moreover, a query is parametric when the mapped MongoDb query includes one or more $arg and $fk operators:

  • $arg: allows to use the arguments of the GraphQL query in the MongoDb query;

  • $fk: allows to map a GraphQL field with a MongoDB relation, specifying which is the document field that holds the relation.

For example, having the following GraphQL schema:

type User {
  id: Int!
  name: String
  posts: [Post]
}

type Post {
  id: Int!
  text: String
  category: String
  author: User
}

type Query {
  usersByName(_name: String!, _limit: Int = 0, _skip: Int = 0): [Users]
}

with MongoDB data organized in the two collections users and posts`:

USERS

{
  "_id": {"$oid": "6037732f5fa7d52581015ed9" },
  "firstName": "Foo",
  "lastName": "Bar",
  "contacts": { "phone": "+39113", "emails": ["foo@domain.com", "f.bar@domain.com"],
  "posts_ids": [ { "$oid": "606d963f74744a3fa6f4489a" }, { "$oid": "606d963f74744a3fa6f4489e" } ] }
}

POSTS

[
  { "_id": {"$oid": "606d963f74744a3fa6f4489a" },
    "text": "Lorem ipsum dolor sit amet",
    "category": "front-end",
    "author_id": {"$oid": "6037732f5fa7d52581015ed9" }
  },
  { "_id": {"$oid": "606d963f74744a3fa6f4489e" },
    "text": "Lorem ipsum dolor sit amet",
    "category": "back-end",
    "author_id": {"$oid": "6037732f5fa7d52581015ed9" }
  }
]

then, possible mappings are:

{
  "descriptor": "...",
  "schema": "...",
  "mappings": {
    "User": {
      "posts": {
        "db": "restheart",
        "collection": "posts",
        "find": { "_id": { "$in": { "$fk": "posts_ids" } } }
      }
    },
    "Post": {
      "author": {
        "db": "restheart",
        "collection": "user",
        "find": { "_id": { "$fk": "author_id" } }
      }
    },
    "Query": {
      "usersByName": {
        "db": "restheart",
        "collection": "users",
        "find": { "name": { "$arg": "_name" } },
        "limit": { "$arg": "_limit" },
        "skip": { "$arg": "_skip" },
        "sort": { "name": -1 }
      }
    }
  }
}

As result, we are saying that:

  • given a User, his posts are the MongoDB documents, within the posts collection, with value of field _id that falls in the posts_ids array of `User’s document;

  • given a Post, its author is the MongoDB document, within the users collection, with value of field _id equal to author_id of `Post’s document;

  • asking for userByName GraphQL field, the MongoDB documents searched are the ones within the users collection with field name equal to value of _name GraphQL argument. Moreover, we are asking to return at most _limit documents, to skip the firsts _skip ones and to sort them by name in reverse order.

Note
you can use also the dot notation with the $fk operator.

Field to Aggregation mapping

You can map a GraphQL field with a MongoDB query using the following parameters:

  • db (String): database name;

  • collection (String): collection name;

  • stages (Array): array of aggregation stages.

As with field to query mapping, $arg and $fk operators are allowed in aggregation stages.
Referring to the previous example of mapping, the following aggregation stages are possible:

...,
"Query": {
    ....,
    "countPostsByCategory": {
      "db": "restheart",
      "collection": "users",
      "stages": [
        { "$group": { "_id": "$category", "count": { "$count": {} } } }
      ]
    }
  }

And the Query in the GraphQL schema will now have the following field:

type Stats {
  _id: String
  count: Int
}

type Query {
  countPostsByCategory: [Stats]
}

Bson types

All primitive GraphQL types have been mapped to corresponding BSON types plus a set of custom GraphQL scalars types have been added:

GraphQL type

Bson Type

Example

Boolean

BsonBoolean

b: true

String

BsonString

s: "foo"

Int

BsonInt32

n: 1

Long

BsonInt64

n: { "$numberLong": "10000000000000000000" }

Float

BsonDouble

n: { "$numberDouble": "1.0" }

Decimal128

BsonDecimal128

n: { "$numberDecimal": "123.456" }

ObjectId

BsonObjectId

{ "$oid": "618d18d6d058286395bb5567" }

Timestamp

BsonTimestamp

ts: { "$timestamp": {"t": 1, "i": 1} }

DateTime

BsonDate

d: { "$date": 1639666957000 }

Regex

BsonRegex

r: { "$regex": "<sRegex>", "$options": "<sOptions>" }

BsonDocument

BsonDocument

doc: { "any": 1, "possible": 1, "document": 1 }

Example

The following GraphQL type User defines the property _id to be of type ObjectId

type User {
    _id: ObjectId
    name: String
    surname: String
    email: String
    posts: [Post]
}

Queries

Up to now, only GraphQL Query can be made, so no subscription or mutation. In order to make a query you can use HTTP request with POST method and both content-type application/json and application/graphql. For instance:

application/json

POST /graphql/<app-uri> HTTP/1.1
Host: <host-name>
Content-Type: application/json

Request body

{
  "query": "query test_operation($name: String){ userByName(_name: $name){name posts{text}} }",
  "variables": { "name": "..." },
  "operationName": "..."
}

application/graphql

POST /graphql/<app-uri> HTTP/1.1
Host: <host-name>
Content-Type: application/graphql

Request body

{
  userByName(_name: "...") {
      name
      posts {
        text
      }
  }
}

Limitations

The GraphQL service has the following limitations:

  • Read-only API: mutations are not supported; the GraphQL API is only intended for simplifying data fetching. To write data, the REST API must be used.

  • Not-supported schema keywords: the schema resolvers do not support the following keywords: enum, union, interface and input.

Response codes

In the following table are reported possible RESTHeart GraphQL Service responses:

HTTP Status code

description

200

It’s all OK!

400

Invalid GraphQL query (e.g. required fields are not in the schema, argument type mismatch), schema - MongoDB data type mismatch, invalid app definition

401

Unauthorized

404

There is no GraphQL app bound to the requested endpoint

405

HTTP method used not supported

500

Internal Server Error

Example responses

200 - OK

{
  "data":{
    "userByName":[
      {
        "firstName": "nameUser1",
        "lastName": "surnameUser1"
      },
      {
        "firstName": "nameUser2",
        "lastName": "surnameUser2"
      }
    ]
  }
}

400 - Bad Request - Invalid GraphQL Query / schema - MongoDB data type mismatch

{
  "data": "...",
  "errors" : "..."
}

400 - Bad Request - Invalid GraphQL App Definition

{
  "http status code":  400,
  "http status description":  "Bad Request",
  "message":  "..."
}

405 - Method Not Allowed

{
  "http status code":  405,
  "http status description":  "Method Not Allowed"
}

500 - Internal Server Error

{
  "http status code":  500,
  "http status description":  "Internal Server Error"
}