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

Edit Page

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
  # app definitions are cached. this sets the time to live in msecs
  app-def-cache-ttl: 10_000
  # 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
  # The time limit in milliseconds for processing queries. Set to 0 for no time limit.
  query-time-limit: 0
  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

GraphQL types are connected to MongoDB data through mappings.

Mappings are applicable to a range of types, including Object, Query, enum, interfaces, and union.

Field to Field mapping

You can map a GraphQL Object field with a specific MongoDB document field or with an element of a MongoDB array. To map nested fields use the dot-notation.

An example follows:

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

In this configuration:

The name field is linked to the MongoDB document’s firstName field. The phone field is associated with the phone field within the nested contacts document. The email field is connected to the first element of the emails array within the nested contacts document.

Note
if you don’t explicitly define a mapping for a field, RESTHeart will automatically map it to the MongoDB document field with the same name.

Field to Query mapping

You can establish a mapping between a GraphQL Object field and a MongoDB query using the following parameters:

  • db (String): The name of the database.

  • collection (String): The name of the collection.

  • find (Document): The selection filter using query operators such as $in, $and, $or, and others.

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

  • skip (Document or Integer): The number of documents to skip among those resulting from the query.

  • limit (Document or Integer): The maximum number of documents to return among those resulting from the query.

Note
It’s important to note that unlimited queries are not allowed. If the query doesn’t specify a limit, the service configuration’s default-limit is applied. Additionally, the limit cannot exceed the max-limit.

Field to Aggregation mapping

You can link a GraphQL Object field with a MongoDB aggregation using the following parameters:

  • db (String): The name of the database.

  • collection (String): The name of the collection.

  • stages (Array): An array of aggregation stages.

Similar to field-to-query mapping, you can utilize $arg and $fk operators within aggregation stages. In reference to the previous mapping example, 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]
}

Optional Stages

Starting from RESTHeart v7.6, field-to-aggregation mapping can include optional stages that are executed only when one or more arguments are specified. This feature enables the handling of optional GraphQL arguments.

Note
Optional Stages are available from RESTHeart v7.6.

Optional Stages in field-to-aggregation mapping are similar to Optional Stages in regular aggregations. The main difference lies in the conditional operators used. In field-to-aggregation mapping, the optional stage operator is $ifarg, whereas in regular aggregations, it is $ifvar.

For a more in-depth understanding of how to use optional stages in both field-to-aggregation mapping and regular aggregations, please refer to the Aggregation documentation.

Mappings operators

Field to Query and Field to Aggregation mappings provide the flexibility to employ the $arg and $fk operators:

  • $arg: This operator enables the utilization of GraphQL arguments within mappings, enhancing dynamic query or aggregation generation.

  • $fk: It allows the specification of the document field responsible for holding a relation. It enables traversing related documents.

For instance, consider 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": "foo",
  "firstName": "Foo",
  "lastName": "Bar",
  "contacts": { "phone": "+39113", "emails": ["foo@domain.com", "f.bar@domain.com"] },
  "posts_ids": [ 1, 2 ]
}

POSTS

[
  { "_id": 1,
    "text": "Lorem ipsum dolor sit amet",
    "category": "front-end",
    "author_id": "foo"
  },
  { "_id": 2,
    "text": "Lorem ipsum dolor sit amet",
    "category": "back-end",
    "author_id": "foo"
  }
]

The 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 a result of using these mapping operators:

  • When given a User, their posts are represented by the MongoDB documents within the posts collection. These documents have an _id field value that matches any of the _id values within the posts_ids array in the `User’s document.

  • When given a Post, its author is identified by the MongoDB document within the users collection. This document has an _id field value that matches the author_id within the `Post’s document.

  • For the userByName GraphQL field, the MongoDB documents being queried are those within the users collection with a name field equal to the value specified in the _name GraphQL argument. Furthermore, you can specify that you want to return a maximum of _limit documents, skip the first _skip documents, and have them sorted by name in reverse order.

Note
the $fk and $arg operators allow the usage of dot notation to traverse document fields.
Note
the $arg operator allows dot notation from RESTHeart v7.6 onwards.

Dot Notation support for $arg and $fk

The $fk and $arg operator can utilize dot notation to access nested properties.

Note
Dot Notation support for $arg is available from RESTHeart v7.6

The Dot Notation support for $arg feature enables the handling of InputTypes. The following example will clarify:

input Filters {
  type: String
  author: String
}

type Query {
  getPosts(filters: Filters!): [Post]
}

The Query mapping can use the dot notation as follows to cope the Filters InputType:

{ "mappings": {
    "Query": {
        "getPosts": {
            "db": "restheart",
            "collection": "the-posts",
            "find": { "author": { "$arg": "filters.author" }, "type": { "$arg": "filters.type" } }
        }
    }
}

Arguments with Default Values

The $arg operator can specify a default value. This default value is utilized when an optional argument is not provided in the GraphQL Query.

Note
Arguments with Default Values are available from RESTHeart v7.6.

Arguments with Default Values in GraphQL mappings are similar to those in regular aggregations. The primary distinction lies in the operators used. In GraphQL mappings, the operator is $arg, whereas in regular aggregations, it is $var.

For example, the following code specifies the default value Andrea for the argument name: {"$arg": [ "name", "Andrea"]}.

For a more comprehensive understanding of how to use arguments with default values, please refer to the Aggregation documentation.

The rootDoc argument

Note
rootDoc is available from RESTHeart v7.6
Tip
more details about this feature are available on github issue 469

The {"$arg": "rootDoc"} argument is a versatile tool that can be employed in both Field to Query and Field to Aggregation mappings.

It enables the utilization of properties from the root document when crafting queries and aggregations.

The root document, in this context, is the first document retrieved from the source.

To provide a clear example, let’s consider a document from the collection authors-and-posts:

The example is implemented in test rootDoc.feature

  {
  "_id": "bar",
  "sub": {
    "posts": [
      { "content": "ping", "visible": true },
      { "content": "pong", "visible": true },
      { "content": "invisible", "visible": false }
    ]
  }
}

And the following GraphQL schema. Note that the field post has the argument visible.

type User {
  _id: String
  posts(visible: Boolean): [Post]
}
type Post {
  content: String
}
type Query {
  users: [User]
}

In order to filter the nested posts objects according to the argument visible we can make use of field to aggregation mapping:

{
  "User": {
    "posts": {
      "db": "restheart",
      "collection": "authors-and-posts",
      "stages": [
        { "$match": { "_id": { "$arg": "rootDoc._id" } } },
        { "$unwind" : "$sub.posts"  },
        { "$replaceRoot": {"newRoot": "$sub.posts"} },
        { "$match": { "visible": { "$arg": "visible" } } }
    ]
    }
  }
}

The field to aggregation mapping selects the root user using the rootDoc and filters the objects in the nested array sub.posts that match the argument visible.

Enum mappings

Note
available from v7.2

Enum type mappings serve to define the correspondence between values in MongoDB and the corresponding enum values.

However, it’s essential to note that enum mappings are optional. When omitted, it is assumed that the value in the database is identical to the string representation of the enum value.

For instance, consider 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 in GraphQL is an abstract type that specifies a particular set of fields that any concrete type implementing the interface must include.

To determine which concrete type a value belongs to when querying against the interface, a TypeResolver must be defined in the interface mappings.

Let’s consider an example involving an 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 serves as an object that establishes a mapping between the names of concrete types (such as InternalCourse and ExternalCourse) and corresponding predicates. These predicates are evaluated against a document, and if a predicate returns true, the GraphQL type associated with that predicate is used to represent the document.

This mechanism allows for dynamic determination of the GraphQL type for a document based on the conditions defined in the predicates. It’s a powerful way to handle polymorphism and resolve the actual type of objects when querying against an interface.

$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 in GraphQL are similar to interfaces in that they represent a way to include multiple types in a single field. However, unlike interfaces, union types do not specify any fields that the types within the union must have in common.

With union types, you can specify that a field can return values of different types, and you can use this construct when you want to retrieve data that doesn’t share a common set of fields but still needs to be represented as a single field in your schema. This is particularly useful for scenarios where you have different types of data that can be queried together under one field, even if they have different structures.

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

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.

Response

Starting from RESTHeart v7.6, the GraphQL API always responds with the content type application/graphql-response+json, following the GraphQL Over HTTP specs.

Possible Response Codes:

HTTP Status Code Description

200

A valid GraphQL response has been generated, even if it contains errors (partial data).

400

The request is invalid (e.g., incorrect JSON, malformed GraphQL query, non-existent fields in selection, etc.) or when the response only contains errors (i.e., data: null).

404

The GraphQL app does not exist.

405

Incorrect method used (not POST or OPTIONS).

408

Request timed out due to the query-time-limit option.

500

Connection error with MongoDB.