GraphQL Queries

GraphQL queries are generated dynamically using javascript

Magento exposes a GraphQL API, which ScandiPWA builds upon and uses to fetch and mutate data. GraphQL is more flexible than REST, and offers a type checking system.

You might want to read the GraphQL documentation to get a full understanding of the language, but we do provide a quick introduction below.

Introduction to GraphQL

GraphQL queries look like JSON objects without quotes or values, and describe which fields you want to fetch from the server:

{
categoryList {
name
children {
name
url
product_count
}
}
}

In this case, we want to query the categoryList field, and fetch its name and children fields. For the children, we want to fetch the name, URL, and product count. It is not clear from the query, but if you were to look at the GraphQl schema, you would know that children is actually an array of such objects.

If we make the above request to the GraphQL endpoint on a Magento instance (it's usually /graphql), we would get the following JSON response:

{
"data": {
"categoryList": [
{
"name": "Default Category",
"children": [
{
"name": "Audio, Video & Photo Equipment",
"url": "/audio-video-photo-equipment.html",
"product_count": 0
},
{
"name": "Home Security & Automation",
"url": "/home-security-automation.html",
"product_count": 0
},
{
"name": "Computers, Peripherals & Accessories",
"url": "/computers-peripherals-accessories.html",
"product_count": 1
},
{
"name": "Office",
"url": "/office.html",
"product_count": 2
},
{
"name": "Telecom & Navigation",
"url": "/telecom-navigation.html",
"product_count": 0
},
// [...]
]
}
]
}
}

The categoryList query has other fields that we could fetch (such as id, description), but GraphQL only returns the ones we asked for.

API

Generating GraphQL Queries

Commonly, when building an application, GraphQL queries are specified in the code as strings, much like the query above. While this method does have its advantages, it is hard to extend - what happens when you want to override a theme and need to fetch one more field from the same query? If queries were hardcoded as strings, you would have to copy and edit the string to create a new query in your theme, which would quickly get hard to maintain. And what about plugins? There would be no easy way to write a plugin that asks for an additional field in some query.

The solution ScandiPWA proposes is to generate queries dynamically. Then, adapting the query to your needs is as easy as using the Override Mechanism on the query's class.

ScandiPWA implements its own library to enable you to generate GraphQL queries and mutations. This is defined in util/Query.

Field

Represents a GraphQL field​

Method

Description

addField(

field: Field|string

) -> this

Adds the specified child field to this field as a subfield. The child field must be either an instance of Field, or a string. If a string is specified, this is equivalent to calling addField(new Field(string))

addFieldList(

fields:(Field|string)[]

) -> this

Adds the specified list of child fields to this field as subfields. Each item of fields must be either an instance of Field, or a string. If a string is specified, this is equivalent to passing new Field(string)

addArgument(

name: string,

type: string,

value: string

) -> this

Adds an argument called name with a value of value of type type to this field. All three parameters must be strings, and are required.

setAlias(

alias: string

) -> this

Aliases the field to the specified alias.

Fragment

Represents a GraphQL fragment. Extends Field, and hence has the same API.

Example

import { Field, prepareFieldString } from 'Util/Query';
​
const childrenField = new Field('children')
.addFieldList([
'name',
'url',
'productCount'
]);
​
const query = new Field('categoryList')
.addArgument('filter', 'CategoryFilterInput', {})
.addField('name')
.addField(childrenField)
.setAlias('categories');
​
console.log(prepareFieldString(query));

Resulting Query (argument valus are sent separately):

categories:categoryList(filter:$filter_1){name,children{name,url,productCount}}

Making Queries

Functions that make requests are defined in util/Request. These implement a smart caching mechanism to improve performance and reduce load.

Function

Description

fetchQuery(

query: Field|Field[]

) -> Promise<Request>

Fetches a query or array of queries, and returns a Promise that resolves when the query successfully completes. If the query fails, so does the Promise.

fetchMutation(

query: Field|Field[]

) -> Promise<Request>

Fetches a mutation or array of mutations, and returns a Promise that resolves when the mutation successfully completes. If the mutation fails, so does the Promise.

QueryDispatcher

QueryDispatcher is a base class for redux dispatchers that can simplify making queries. Dispatchers that need to make queries typically need to make the request, and then handle the resulting data or any errors. QueryDispatcher automates this this by implementing a handleData function that performs this logic.

A subclass extending QueryDispatcher will need to define 3 functions. Once these 3 functions are defined, handleData will work automatically as expected.

  • prepareRequest(options, dispatch), a function that returns the query that the dispatcher wants to make

  • onSuccess(data, dispatch), a function that is called with the response data when the query completes successfully

  • onError(error, dispatch), a function that is called on request error

Note that all 3 functions get access to Redux's dispatch function in case they need to use it (e.g. to show a notification).

GraphQL in ScandiPWA

To keep the codebase organized, we don't want the components or redux dispatchers to be responsible for generating queries. Instead, we keep all query-generating code in query.

By convention, query contains 1 JavaScript file for each group of related queries. For example, the Cart.query.js file contains queries relating to querying or mutating the customer's cart. All of these files define classes with one or more functions to generate some query, as well as "private" helper methods (optionally). These classes are exported as singleton instances intended to be used by the rest of the app. For an example, consider the Cart.query.js file:

query/Cart.query.js (simplified, annotated)
import { isSignedIn } from 'Util/Auth';
import { Field } from 'Util/Query';
​
/** @namespace Query/Cart */
export class CartQuery {
// creates a query for getting cart data
// caller method expected to provide certain arguments
getCartQuery(quoteId) {
const query = new Field('getCartForCustomer')
.addFieldList(this._getCartTotalsFields())
.setAlias('cartData');
​
// since queries are generated dynamically, we can add different
// arguments based on certain conditions
if (!isSignedIn()) {
query.addArgument('guestCartId', 'String', quoteId);
}
​
return query;
}
​
getSaveCartItemMutation(product, quoteId) {
const mutation = new Field('saveCartItem')
.addArgument('cartItem', 'CartItemInput!', product)
.addFieldList(this._getSaveCartItemFields(quoteId));
​
if (!isSignedIn()) {
mutation.addArgument('guestCartId', 'String', quoteId);
}
​
return mutation;
}
​
getRemoveCartItemMutation(item_id, quoteId) {...}
​
getApplyCouponMutation(couponCode, quoteId) {...}
​
getRemoveCouponMutation(quoteId) {...}
​
// [...] helper methods not intended for public use
}
​
export default new CartQuery();

When a query generation file is defined, it can be used to make requests in dispatchers as well as component's containers.