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

The default configuration for the graphql plugin is as follows.

graphql:
  uri: /graphql
  db: restheart
  collection: gql-apps
  # app cache can be disabled if needed, such as during testing or development
  app-cache-enabled: true
  # app cache entries are automatically revalidated every TTR milliseconds
  app-cache-ttr: 60_000 # in msecs
  # 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
Note
app-cache-enabled and app-cache-ttr are available from v8.0.9 and v8.0.11, respectively. Earlier versions use an expiring cache policy with TTL configurable via the now-deprecated graphql/app-def-cache-ttl option. See issue #523.

GraphQL App Definition

Each GraphQL application requires a GraphQL App Definition, a JSON document typically stored in the /graphql collection.

GraphQL applications can be dynamically created or updated by modifying the corresponding document in MongoDB.

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]
}

Starting with RESTHeart 8.2, the @user predefined argument is now available in aggregation stages when using Field-to-Aggregation mapping. This enables dynamic filtering based on the authenticated user. For example, the following mapping is supported:

type User {
  _id: String,
  name: String
}

type Query {
  me: User
}

# Mapping: 'me' always returns the authenticated user
"Query": {
  "me": {
    "db": "restheart",
    "collection": "users",
    "stages": [
      {
        "$match": {
          "$or": [
            { "_id": { "$arg": ["@user._id", null] } },
            { "_id": { "$arg": ["@user.sub", null] } }
          ]
        }
      }
    ]
  }
}

With this configuration, the me query will return the User object corresponding to the authenticated user.

Optional Stages

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.

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.

Dot Notation support for $arg and $fk

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

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.

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

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

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

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

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
      }
  }
}

App Definition Caching

The caching of GQL App Definitions is automatically handled by the system.

All existing GQL apps are cached at startup. This behavior can be bypassed by disabling the graphAppsInitializer plugin, in which case each GQL app is cached upon its first request.

When a GQL app definition is created or updated, the cache on the RESTHeart instance handling the request is automatically updated.

In multi-instance deployments, the caches on other nodes are refreshed after a configurable Time to Revalidate (TTR) interval (configuration option /graphql/app-cache-ttr with default value of 60 seconds) by the graphAppsUpdater. This is also in charge of removing from the cache deleted apps.

For development or testing in multi-node setups, caching can be disabled by setting graphql/app-cache-enabled to false

Additionally, a new provider, gql-app-definition-cache, has been introduced. This provider is available to other plugins, allowing them to customize and extend the cache invalidation policy as needed.

Example usage

@Inject("gql-app-definition-cache")
LoadingCache<String, GraphQLApp> gqlAppDefsCache;

public void invalidateAll() {
    this.gqlAppDefsCache.invalidateAll();
}

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

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.