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 betrue
orfalse
. -
uri
: specifies at which endpoint your GraphQL application is reachable (e.g./graphql/uri
). If theuri
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 theposts
collection. These documents have an_id
field value that matches any of the_id
values within theposts_ids
array in the `User’s document. -
When given a
Post
, its author is identified by the MongoDB document within theusers
collection. This document has an_id
field value that matches theauthor_id
within the `Post’s document. -
For the
userByName
GraphQL field, the MongoDB documents being queried are those within theusers
collection with aname
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 |
|
boolean |
|
boolean |
|
boolean |
|
checks if the type document contains the specified keys. Dot notation and multiple keys are permitted as in |
|
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 |
|
checks if the type value is equal to the given argument. The argument can be any JSON as in |
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 |
|
field |
|
field |
|
field |
|
field |
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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., |
404 |
The GraphQL app does not exist. |
405 |
Incorrect method used (not POST or OPTIONS). |
408 |
Request timed out due to the |
500 |
Connection error with MongoDB. |