Looking for Cloud Services or Professional Support? Check restheart.com

GraphQL API

Overview

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

The GraphQL API works side by side with the REST API to build modern applications.

Configuration

For each GraphQL application you need to define a so called GraphQL app definition. This is a JSON document to be created by default in the /graphql collection.

The GraphQL plugin default configuration follows: It also allows to specify the collection that holds the app definitions.

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

The GraphQL application can be dynamically created and updated by simply creating or updating the related document in 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.

  • uri: specifies at which endpoint your GraphQL application is reachable (e.g. /graphql/uri). If the uri parameter is missing the application’s name is used instead.

{
  "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 are mapped to MongoDB data. For each GraphQL type you should define a mapping object.

Object Mappings

The mappings for an Object require to define a property for each field.

The following mappings types are available:

  • Field to Field mapping

  • Field to Query mapping

  • Field to Aggregation mapping

Field to Field mapping

You can map a GraphQL field with a specific MongoDB document field or with an element of a MongoDB array. To map nested fields use the 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
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.

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:

  • given a User, his posts are the MongoDB documents, within the posts collection, with value of field _id that are 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 aggregation 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]
}

Enum mappings

Note
available from v7.2

The mappings for Enum types specify which value in MongoDB maps to which enum value.

The enum mappings are optional. If omitted, the value in the db is supposed to be the same string than the enum value.

For example, the following enum:

enum Level { ENTRY, MEDIUM, ADVANCED }

Can be mapped to numeric values as follows:

"Level": {
    "ENTRY": 0,
    "MEDIUM": 1,
    "ADVANCED": 2
}
Note
An example GraphQL application that uses enum is enumTestApp.json used in the test enum.feature

Interface mappings

Note
available from v7.2

An Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface.

In order to decide which type a concrete value belongs to, a TypeResolver must be defined in the interface mappings.

Given the following Interface and concrete Objects

interface Course { _id: ObjectId, title: String }
type InternalCourse implements Course { _id: ObjectId, title: String }
type ExternalCourse implements Course { _id: ObjectId, title: String, deliveredBy: String }
type Query { AllCourses: [Course] }

The following mappings defines the TypeResolver using the $typeResolver keyword.

"Course": {
    "$typeResolver": {
        "InternalCourse": "not field-exists(deliveredBy)",
        "ExternalCourse": "field-exists(deliveredBy)"
    }
}

The $typeResolver is an object that maps the concrete types' names (InternalCourse and ExternalCourse) with predicates. If the predicate evaluates to true against a document than its GraphQL type is used for it.

$typeResolver can use the following predicates:

predicate

description

and

boolean and operator

or

boolean or operator

not

boolean not operator

field-exists

checks if the type document contains the specified keys. Dot notation and multiple keys are permitted as in field-exists(foo.bar, bar.foo)

field-eq

checks if the specified type key is equal to a value. The key can use the dot notation and the value can be any JSON as in field-eq(field=foo.bar, value='{ "n": 1 }').

value-eq

checks if the type value is equal to the given argument. The argument can be any JSON as in value-eq('{ "n": 1 }').

Warning
the value of the field-eq predicate must be valid JSON. In particular pay attention to string values that require two quotes as in field-eq(field=foo, value='"bar"').

Examples of field-eq predicates

predicate

condition

field-eq(field=n, value=100)

field n equals number 100

field-eq(field=n, value='"100"')

field n equals string "100"

field-eq(field=b, value=true)

field b equals boolean value true

field-eq(field=o, value='{ "bar": 1 }')

field o equals JSON Object { "bar": 1 }

field-eq(field=s, value='"foo"')

field s equals string "foo"

Note
An example GraphQL application that uses interface is interfaceTestApp.json used in the test interface.feature

Union mappings

Note
available from v7.2

Union types are very similar to interfaces, but they don’t specify any fields.

union Course = InternalCourse | ExternalCourse
type InternalCourse { _id: ObjectId, title: String }
type ExternalCourse { _id: ObjectId, title: String, deliveredBy: String }

As for interfaces, a TypeResolver must be defined in the union mappings to decide which type a concrete value belongs to.

The format for union’s $typeResolver is identical to interface’s.

Note
An example GraphQL application that uses union is unionTestApp.json used in the test union.feature

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

true

String

BsonString

"foo"

Int

BsonInt32

1

Long

BsonInt64

{ "$numberLong": "10000000000000000000" }

Float

BsonDouble

{ "$numberDouble": "1.0" }

Decimal128

BsonDecimal128

{ "$numberDecimal": "123.456" }

ObjectId

BsonObjectId

{ "$oid": "618d18d6d058286395bb5567" }

Timestamp

BsonTimestamp

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

DateTime

BsonDate

{ "$date": 1639666957000 }

Regex

BsonRegex

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

BsonDocument

BsonDocument

{ "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 input keyword. RESTHeart versions up to 7.1 don’t support the keywords enum, union, interface.

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":  "..."
}