# Query Controller

Query controller is a convenient way to manage GraphQL. This architectural solution helps to easily create, update, and extend GraphQL queries with the [Extension Mechanism](https://scandipwa.gitbook.io/shopify/architecture/extensibility).

## Motivation

We wanted an easy way to define fields for GraphQL queries that won't require writing query string, but will rather compose that string automatically, given the supplied data. This would allow for better readability, reusability, and most importantly - extensibility. We wanted the Query Controller to:

* Make the process of query declarations more intuitive and simple,
* Be modular to allow for extensibility,
* Give the option to define query fields conditionally,
* Support main GraphQL features, such as aliases and arguments

## Usage

Query controllers look and behave just like a regular Javascript classes:

```javascript
export class CollectionsQuery {
    _getCollectionFields() {
        return [
            'description',
            'descriptionHtml',
            'handle',
            'title',
            new Field('image').addFieldList([
                new Field('transformedSrc').setAlias('src'),
                new Field('altText').setAlias('alt')
            ])
        ];
    }

    _getCollectionField() {
        return new Field('node').addFieldList(
            this._getCollectionFields()
        );
    }

    _getEdgesField() {
        return new Field('edges').addFieldList([
            'cursor',
            this._getCollectionField()
        ]);
    }

    _getCollectionsFields() {
        return [
            this._getEdgesField(),
            getPageInfoField()
        ];
    }

    getCollectionsField({ first, after, before }) {
        return new Field('collections')
            .addFieldList(this._getCollectionsFields())
            .addArgument('before', 'String', before)
            .addArgument('after', 'String', after)
            .addArgument('first', 'Int', first);
    }

    getCollectionByHandleField({ handle }) {
        return new Field('collectionByHandle')
            .addArgument('handle', 'String!', handle)
            .addFieldList(this._getCollectionFields());
    }
}

export default new CollectionsQuery();
```

You can immediately spot three things:

* Public methods to get fields for specfic query: `getCollectionByHandleField` and `getCollectionsField`. We call them **Query getters**.
* "Private" methods prefixed with underscore to define the fields themselves: `_getCollectionFields`, `_getCollectionField`, `_getEdgesField`, and `_getCollectionsFields`. We call them **Field getters**.
* `Field` that is used in every single one of these methods.

### Field

`Field` is our way of storing GraphQL field data. It's a regular Javascript class and you can read more on `Field` as well as other GraphQL features supported by `@scandipwa/graphql` in the [documentation](https://docs.scandipwa.com/building-blocks/constructing-graphql-queries#graphql-in-scandipwa).

### Query getters

Query getters are used to compose a GraphQL query (or mutation) from given arguments, if any. Query getters are *only* responsible for:

1. Returning a field that exists in the Query/Mutation root of the target GraphQL schema
2. Applying and/or passing down arguments for the query/mutation
3. Referencing the fields declared by Field getters

{% hint style="warning" %}
Query getters are **not** responsible for any field definitions! You shouldn't list the fields directly in  Query getters and instead use Field getters to retrieve the desired fields.
{% endhint %}

✅ How does a good query getter look like:

```javascript
getCollectionByHandleField({ handle }) {
    return new Field('collectionByHandle') // Returns a field from Query root - ✅ 
        .addArgument('handle', 'String!', handle) // Applies arguments - ✅ 
        .addFieldList(this._getCollectionFields()); // Field getter reference - ✅ 
}
```

[❌](https://emojipedia.org/cross-mark/) How does a bad query getter look like:

```javascript
getCollectionByHandleField({ handle }) {
    return new Field('collectionByHandle') // Returns a field from Query root - ✅ 
        .addArgument('handle', 'String!', handle) // Applies arguments - ✅ 
        .addFieldList(['description', 'title']); // Declares fields directly - ❌ 
}
```

### Field getters

Field getters are used to define the fields that will be used by controller's queries, they are never called directly in the app. Field getters should be:

1. Modular to allow for extensibility. Meaning that if there are multiple GraphQL types involved in the queries, each of them should be described in a separate private method
2. Returning a field or an array of fields. No fields from the Query/Mutation root are permitted.

✅ How does a good field getter look like:

```javascript
_getEdgesField() {
    return new Field('edges').addFieldList([ // Returns a field - ✅
        'cursor',
        this._getCollectionField() // References other Field getters - ✅
    ]);
}
```

[❌](https://emojipedia.org/cross-mark/) How does a bad field getter look like:

```javascript
_getEdgesField() {
    const edgesField = new Field('edges').addFieldList([ // Returns a field - ✅
        'cursor',
        this._getCollectionField() // References other Field getters - ✅
    ]);
    return new Field('collections') // Returns a field from Query root - ❌
        .addFieldList(edgesField); // Declares reusable fields - ❌
}
```

## TypedQuery

`TypedQuery` allows to map **Query getters** to a function and offers more deep extension possiblities. This how a Query controller with `TypedQuery` looks like:

```javascript
import { mapQueryToType, TypedQuery } from '@scandipwa/shopify-api/src/util/TypedQuery';

export const PAGINATED_COLLECTIONS = 'paginated';
export const SINGLE_COLLECTION = 'single';

export class CollectionsQuery extends TypedQuery {
    typeMap = {
        [PAGINATED_COLLECTIONS]: this.getCollectionsField.bind(this),
        [SINGLE_COLLECTION]: this.getCollectionByHandleField.bind(this)
    };

    _getCollectionFields() { ... }

    _getCollectionField() { ... }

    _getEdgesField() { ... }

    _getCollectionsFields() { ... }

    getCollectionsField({ first, after, before }) {
        return new Field('collections')
            .addFieldList(this._getCollectionsFields())
            .addArgument('before', 'String', before)
            .addArgument('after', 'String', after)
            .addArgument('first', 'Int', first);
    }

    getCollectionByHandleField({ handle }) {
        return new Field('collectionByHandle')
            .addArgument('handle', 'String!', handle)
            .addFieldList(this._getCollectionFields());
    }
}

export default mapQueryToType(CollectionsQuery);
```

There are several differences from a regular Query controller definition:

1. The Query controller now extends the `TypedQuery` class
2. New member property `typeMap` is present
3. The default export is no longer a class instance, but the `mapQueryToType` function.

### Query types

The usage of `TypedQuery` involves the assignment of type to each **Query getter** method. In the example above, we map `getCollectionsField` to the `PAGINATED_COLLECTIONS` type and `getCollectionByHandleField` to the `SINGLE_COLLECTION` type:

```javascript
typeMap = {
    [PAGINATED_COLLECTIONS]: this.getCollectionsField.bind(this),
    [SINGLE_COLLECTION]: this.getCollectionByHandleField.bind(this)
    // ^ Query type ^                 ^ Query getter ^
};
```

In addition to `typeMap`, `TypedQuery` class defines `this.currentType` property, which will contain the type of the **Query getter**. If the query of `SINGLE_COLLECTION` type was called, `this.currentType` will be `'single'`.

The knowledge of query type allows to conditionally add or remove fields from **Field getters** depending on the query, let's take product images as an example. We would want product page to contain 15 product images, but we would also want product listing page to only show one image. Following all the rules for creating Query controllers we would get:

```javascript
_getImagesField() {
    return new Field('images')
        .addArgument('first', 'Int', 15) // Number of images to fetch
        .addField(new Field('edges')
            .addField(new Field('node')
                .addFieldList(this._getImagesFields())));
}
```

This leads to an obvious problem, as we would fetch 15 images for each product on the product listing page. We would be fetching unnecessary information and making our requests slower. This is a great place to implement `TypedQuery`:

```javascript
_getImagesField() {
    const SINGLE_PRODUCTS_IMAGES = 15;
    const PAGINATED_PRODUCTS_IMAGES = 1;

    // Value of "first" depends on the query type
    const first = this.currentType === SINGLE_PRODUCT
        ? SINGLE_PRODUCTS_IMAGES // One product was queried = 15 images
        : PAGINATED_PRODUCTS_IMAGES; // Multiple products were queried = 1 image

    return new Field('images')
        .addArgument('first', 'Int', first) // number of images to fetch
        .addField(new Field('edges')
            .addField(new Field('node')
                .addFieldList(this._getImagesFields())));
}
```

Now that we have access to `this.currentType`, it is possible to conditionally change the number of images that we want to fetch based on the **Query getter**. In this example, we would fetch 15 images for single product query and only 1 for paginated product query.

### Getting query by type

As mentioned before, Query controllers that extend `TypedQuery` must `mapQueryToType(MyQuery)` as a default export. In order to get a query by type, you will to call call the exported function as pass the query type as an argument:&#x20;

```javascript
const getCollectionQueryByType, { SINGLE_COLLECTION } from '@scandipwa/shopify-collections/src/api/Collections.query';

const queryGetter = getCollectionQueryByType(SINGLE_COLLECTION);
```

## Extension

### Why would you extend?

You would want to extend Query controllers for various reasons:

1. Adding new fields to GraphQL queries made by the Query controller
2. Defining brand new GraphQL query type  &#x20;

### Examples

Learn how to extend Query controllers with these examples taken directly from the code.

#### Add new fields to a Field getter

```javascript
import ProductTagsQuery from '../api/ProductTags.query';

const addTagsField = (args, callback) => {
    const originalProductFields = callback(...args);

    return [
        ...originalProductFields,
        'tags'
    ];
};

export default {
    'ShopifyProducts/Api/Products/Query/ProductsQuery': {
        'member-function': {
            _getProductFields: addTagsField
        }
    }
};

```

#### Add a new query type to typeMap

```javascript
import ProductRecommendationsQuery from '../api/ProductRecommendations.query';

export const PRODUCT_RECOMMENDATIONS = 'product_recommendations';

const addProductRecommendationsQuery = (member, instance) => ({
    ...member,
    [PRODUCT_RECOMMENDATIONS]: ProductRecommendationsQuery.getRecommendedProductsField.bind(instance)
});

export default {
    'ShopifyProducts/Api/Products/Query/ProductsQuery': {
        'member-property': {
            typeMap: addProductRecommendationsQuery
        }
    }
};

```
