On the The GraphQL schema page, we gave the definitions of query and mutation relative to SDL. Now let’s focus on the query language and see how to formulate and execute a GraphQL call in practice.
Generated endpoints #
For each application view (Application Schema) defined in the engine, a GraphQL endpoint is generated available at:
https://<CloudletURL>/auth/api/graphql/<ApplicationName>
Each endpoint references a separate GraphQL API. These endpoints remain constant throughout the life cycle of the generated application, possibly changing if you regenerate the application after replacing or modifying the original engine.
<CloudletURL>
depends on the domain on which the Cloudlet is deployed, the username
of the owner account, and the name of the Cloudlet. For example, given the Administration
view on the Workforce
Cloudlet of the JohnDoe
account, the corresponding GraphQL endpoint is:
https://hs4.fhoster.com/JohnDoe/Workforce/auth/api/graphql/Administration
A list of all endpoints generated for the Cloudlet is available in the API & Public URLs panel, which can be reached from the Dashboard by clicking on the .
Communicating with GraphQL #
In REST, the type of HTTP request determines the type of operation to be performed. GraphQL calls, on the other hand, are always POST
type requests regardless of the operation, as they always contain a body encoded in JSON.
The easiest way to query the API is via GraphiQL. You can also use cURL or any other HTTP library. With cURL, for example, the JSON payload must contain a query
key, having as value the query written in the GraphQL language:
curl -i -X POST $MY_ENDPOINT \
-H "Content-type: application/json" \
-H "Authorization: Basic $MY_TOKEN" \
-d '{"query": "'"$MY_QUERY"'"}'
Authentication #
All registered Cloudlet users can communicate with GraphQL. As with all Cloudlet resources under /auth
, authentication in GraphQL is Basic
and requires you to provide username
and password
. In the case of the Cloudlet user-administrator, these credentials match those of Cloudlet’s proprietary Livebase account.
At a low level, every HTTP request to the API must include the header Authorization: Basic <MyToken>
; the token is the result of Base64 encoding of the string <Username>:<Password>
; if you use cURL, you must always specify the header as shown above. Higher-level libraries, such as Apollo, must be configured appropriately by providing the authentication token.
Detail: generating the token
Formulating queries and mutations #
The two types of operation allowed are query and mutation (mutations). Compared to REST, a query is equivalent to a GET
, while mutations are analogous to POST
, PATCH
or DELETE
depending on the implementation. Both queries and mutations share a similar format, with some important differences.
These are the steps required to formulate a GraphQL call:
you specify the operation type, using the keyword
query
ormutation
depending on the operation to be performed. If you want to perform a query and there is no ambiguity in the code, you can omit the keyword altogether;you require a special root object (root), that is the entry point in the schema containing all available services of query (or mutation) type;
on it, you specify the name of the service to be invoked (a service is a field of the root object, which returns a object containing the result of the service invocation);
if the service requires them, you specify any arguments in round brackets next to the service name;
- read services require an ID to retrieve individual objects or options to filter and retrieve paginated results;
- write services and other mutations require an input object containing the data to be sent to the server.
you specify, on the output-object, the fields you want to get from the server. If you want to get a field that is itself an object (with sub-fields), you must expand each node of this type until it reaches a leaf, i.e. a field of type scalar. This ensures that the response always has an unambiguous form. The order of the fields is not relevant.
In its simplest form, a query is structured like this (rows prefixed with #
are not interpreted):
query {
ServiceName {
# set of fields to select
}
}
All intermediate nodes should be expanded to require only scalar (leaves):
query {
QueryServiceName {
scalarField1
scalarField2
nestedField {
scalarField3
}
}
}
A mutation always requires at least one argument specifying what data you want to send to the server:
mutation {
MutationServiceName(mutationArgument: "Hello world!") {
# set of fields to select
}
}
Services of type mutation also return output objects, such as queries, on which one or more scalars are to be selected:
mutation {
MutationServiceName(mutationArgument: "Hello world!") {
scalarField1
scalarField2
nestedField {
scalarField3
}
}
}
The input can also be a complex object:
mutation {
MutationServiceName(data: {first_name: "John", last_name: "Doe", age: 42}) {
scalarField1
scalarField2
nestedField {
scalarField3
}
}
}
Working with variables #
Variables can make GraphQL calls more dynamic and powerful and help reduce the complexity of mutations when passing complex input objects.
Here is an example query with a single variable:
query($number_of_employees: Int!) {
Employee___getPage(
options: {next: $number_of_employees}
) {
items {
full_name
}
}
}
{
"number_of_employees": 30
}
Variables must be defined outside the query in a valid JSON object, including an arbitrary number of variables to be used in the call. To use the variables so declared in the GraphQL operation, you must:
Pass the variables as arguments to the operation:
query($number_of_employees: Int!) {
The argument is a key-value pair, where the key is the name prefixed by
$
, and the value is the type of the variable (e.g.Int
). You can add a!
to mark the argument as mandatory. The type should always be included to comply with GraphQL’s static typing so that the server can validate the variables passed against the schema.Use variables in operation:
Employee___getPage(options: {next: $number_of_employees}) {
In this example, we replace the variable with the number of employees to retrieve.
When sending a call with variables, it is the client’s job to include the data structure containing them in the HTTP POST
, along with the GraphQL query. With cURL, for example, the variables should be included in the JSON payload at the variables
key:
curl -i -X POST $MY_ENDPOINT \
-H "Content-type: application/json" \
-H "Authorization: Basic $MY_TOKEN" \
-d '{"query": "query($number_of_employees: ID!) { Employee___get(_id:$number_of_employees) { full_name } }", "variables": {"id": "10101"}}'
This process makes the query argument dynamic. We can simply change the value in the variables
object and keep the rest of the query unchanged, encouraging reuse.
More information about variables is available here: Variables.
Query example #
Let’s look at a typical query, where we retrieve information from an Employee object using the single read service get
.
{
Employee___get(_id: "12345") {
full_name
age
team {
name
}
assignments {
totalCount
items {
start_date
end_date
project_ {
director
}
}
}
}
}
Let’s go through the query line by line:
- we use the
Query.Employee___get
service, which allows us to get anEmployee
object. This service requires the_id
argument, a scalar of type ID; - of the returned employee, we ask for his full name (
full_name
) and his age (age
), two scalars of type string and number respectively; - we also ask for information about two satellite classes: the fields
team
andassignments
are of typeTeam
andProject_assignment
respectively, so we expand these objects and select scalars; - for
team
(role to one), we ask for the name of the team the employee is part of (name
); - for
assignments
(many roles), we ask for the number of assignments of the employee (totalCount
) and use theitems
field of theProject_assignmentPage
object to select which fields to show for each assignment; - we choose the scalars
start_date
andend_date
, of type date, i.e., start and end date of the assignment, and show the project manager associated with the assignment; - finally, we show the name of the project director associated with the assignment by operating a selection on the object of type
Project
associated with the assignment on the fieldproject_
.
Example of mutation #
Mutations often require information that can only be retrieved by first running a query. In this example, we use a query to retrieve the ID of an existing Employee object and a mutation to update its data, using the getPage
and update
services.
query FindEmployeeID {
Employee___getPage(options: {filter: {team_name___eq: "Cool Coders"}}) {
items {
_id
full_name
address {
_id
city
}
}
}
}
mutation ChangeEmployeeCity {
Employee___update(
data: {
_id: "10101"
supervisor: {
set: "10102"
}
address: {
update: {
_id: "222000"
city: "Milan"
}
}
}
) {
_id
full_name
address {
city
}
supervisor {
_id
full_name
}
}
}
Let’s examine the example: the task is to update the city of residence of an existing employee who has just moved and assign a supervisor who is part of his team. Of course, we know the first and last names of both employees.
- consulting the documentation generated for our schema, we find the
Mutation.Employee___update
service, which needs the inputEmployeeUpdate
, a type that has the field_id
mandatory (marked with the symbol!
); - we need to retrieve the ID of the employee, but we also need the ID of his supervisor because we notice that the
supervisor
field ofEmployeeUpdate
is an input of typeEmployeeRoleRef
(update structure for 1-way role Team on class Employee), within turn the fieldset
, of type ID, to indicate the employee to be associated; - we use the page-reading
Query.Employee___getPage
service to retrieve the two employees, both objects of class Employee; - since we know that both employees are part of the same team, we can restrict the results returned by the paginated get. We include the
options
argument with theEmployeePageOptions
input; here we specify thefilter
field, choosing as valueteam_name___eq
and writing the team name, which we know is"Cool Coders"
; - from the returned object of type
EmployeePage
, we use theitems
field to select which fields to show for each employee. We choose to showfull_name
, and in addition to_id
; we also need the ID of the object part Address.
Running the query, we get the following response:
{
"data": {
"Employee___getPage": {
"items": [
{
"_id": "10101",
"full_name": "Frank Jaeger",
"address": {
"_id": "222000",
"city": "Rome".
}
},
{
}, "_id": "10101",
}, { "full_name": "Levi Smith",
"address": {
"_id": "222754",
"city": "Milan".
}
}
// ... other employees of the same team
]
}
}
}
Having known the IDs, let’s now move on to the mutation:
- as mentioned before, we want to use the
Mutation.Employee___update
service: we populate thedata
argument with an object of typeEmployeeUpdate
, inserting on the mandatory field_id
the ID of our employee, i.e."Frank Jaeger"
; - we add the field
supervisor
as mentioned above, specifying the fieldset
to which we match the ID of the supervisor, namely"Levi Smith"
; - to change the city, we have to modify the object Address (part a 1), which corresponds in
EmployeeUpdate
to the fieldaddress
of typeAddressRoleObject
(update structure for part a 1 to Address on class Employee), within turn the fieldupdate
, of typeAddressUpdate
; - also
AddressUpdate
requires the ID of the object; we provide it together with the fieldcity
to update the city from"Rome"
to"Milan"
; - finally, we specify which fields we want to obtain from the server in output at the end of the update operation. The type returned by
Employee___update
is stillEmployee
and we ask to show the new city and the supervisor’s name, which we expect to be"Milan"
and"Levi Smith"
respectively.
Running the mutation, we get this response:
{
"data": {
"Employee___update": {
"_id": "10101",
"full_name": "Frank Jaeger",
"address": {
"city": "Milan"
},
"supervisor": {
"_id": "10102",
"full_name": "Levi Smith"
}
}
}
}
In this example we worked with a selection of schema fields: we could have asked for a lot more information from those available for the Employee
and Address
types, but thanks to GraphQL, we were able to focus only on those of interest in this context. We also made only two calls to the server, saving time and bandwidth.
One last note: the syntax can become cumbersome when passing multiple fields in an input object. In these cases, it can be helpful to move the fields into a variable. Here’s how we could have rewritten the original mutation using a variable:
mutation ChangeEmployeeCity($myVar: EmployeeUpdate!) {
Employee___update(data: $myVar) {
_id
full_name
address {
city
}
supervisor {
_id
full_name
}
}
}
{
"myVar": {
"_id": "10101",
"supervisor": {
"set": "10102"
},
"address": {
"update": {
}, "_id": "222000",
"city": "Milan".
}
}
}
}
Further reading #
In addition to variables, GraphQL offers many other useful constructs for making calls supported by our implementation but are beyond the scope of this guide. If you want to learn more, you can find more information in the official documentation (graphql.org):