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 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
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 documentfirstName
field -
phone
is mapped with fieldphone
incontacts
nested document -
email
is mapped with 1st element ofemails
array withincontacts
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 theposts
collection, with value of field_id
that are in theposts_ids
array of `User’s document; -
given a
Post
, its author is the MongoDB document, within theusers
collection, with value of field_id
equal toauthor_id
of `Post’s document; -
asking for
userByName
GraphQL field, the MongoDB documents searched are the ones within theusers
collection with fieldname
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 |
|
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
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 keywordsenum
,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": "..."
}