Hi, there! My name is Łukasz, and I'm the Alokai Core Team member. That's my first post here. So let's say that this is a welcoming one. I'd like to share with you some insights from an issue that I was dealing with, it was applying custom GraphQL queries for commercetools integration.
Custom Query!
VSF, as a platform-agnostic software, has some headless commerce tools integrations, one of them is commercetools (easy naming ;))
This commerce management tool use GraphQL client to communicate with the front-end. That means you can query shop data to receive and present it within any user/customer interface.
Usually, a query is defined as a string value and aims to describe which data fields we want to fetch from the database.
Here is a simple example of getProducts query wrapped by the gql tag:
const newQuery = gql`
query products(
$where: String
$sort: [String!]
$limit: Int
$offset: Int
$skus: [String!]
$acceptLanguage: [Locale!]
$currency: Currency!
) {
products(where: $where, sort: $sort, limit: $limit, offset: $offset, skus: $sku)
{
results {
id
}
}
}
`;
Therefore in VSF, those queries are predefined. "Why?" - you might ask... Well, it prevents bad requests and data receiving errors. However, sometimes we want to get more - or less. That's why our users asked for more flexibility in terms of passing custom queries.
And here it is.
Well... one moment. Let's look at this example:
import { useProduct } from '@vue-storefront/commercetools'
const { search } = useProduct()
search({ id: '12345' })
As you can see, the useProduct composable (VSF API) provides a search method to which we can pass some params, such as id. That way we will get data based on our predefined GraphQL query. So let's look at how this one will work with custom query.
search({ id: '12345' }, (query, variables) => ({ query, variables }))
As a second, optional argument of search function you can pass another one that provides query and variables. Here is a more complex example:
const customQuery = (query, variables) => {
// import a custom Graphql query
const newQuery = require('./queries/customQuery.gql')
// // override "acceptLanguage" variable and leave other ones untouched
const newVariables = { ...variabes, acceptLanguage: ['en', 'de'] }
return {
query: newQuery,
variables: newVariables
}
}
search({ id: '12345' }, customQuery)
In this code, you can find query variables as well. Those variables in default shape are passed through VSF API. By destructuring then adding your own you can easily override them.
To illustrate this example more, we can set it within the regular Vue component.
import Vue from 'vue'
const customQuery = require('./queries/customQuery.gql')
import { facetGetters } from '@vue-storefront/commercetools'
export default Vue.extend({
data: (): { products: {}[] } => {
products: [],
},
async created(): void {
// init search
await search({ id: '12345' }, customQuery);
// get products from stored value
this.products = computed(() => facetGetters.getProducts(result.value));
}
})
And that's it. Your custom query is here. For almost all composables. Enjoy.
Custom query and the Alokai Core implementation issue.
So not all of our integrations use GraphQL to communicate with. That means that in some cases we don’t need any query to receive data from the platform - some of them might use REST API. However, we shouldn’t even think about it. That’s why we’ve set this parameter as optional. But how we know when it’s optional or not? After all, our core functions do not know what kind of platform and integration was used to share data with them.
Let’s look at this example:
search(params: {}, customQuery?: any)
It's OK, right? Well, not exactly. Still, even it's optional customQuery is our function called an argument. We should know that we can provide this value (IDE will cover that), but the API should not force it. Finally, what we can do here is to use TypeScript function overloading.
Function overloading
As we read in the official docs: JavaScript is inherently a very dynamic language. It’s not uncommon for a single JavaScript function to return different types of objects based on the shape of the arguments passed in. That's why we can supply multiple function types for the same function as a list of overloads in/with TypeScript. 1
Let's cover that with our case:
// only one argument
function search(params: Record): Promise;
// two arguments with "optional" custom query
function search(params: Record, customQuery: CustomQuery): Promise;
// the function overload implementation
function search(params: Record, customQuery?: CustomQuery): any {
return { params, customQuery }
};
It is how it looks like in IDE, with useProduct composable:
Note that in this case, we have to provide overloaded function implementation. Otherwise:
Error:(112, 10) TS2391: Function implementation is missing or not immediately following the declaration.
There is a better way, though. We can put our overloading into the interface, like that:
interface UseProduct {
...
search(params: SearchParams): Promise;
search(params: SearchParams, customQuery?: CustomQuery): Promise;
...
}
Now, we don't have to define the implementation. Of course, this will not cover all the cases, but it's a more elegant and precise way to provide function overloading in/with TypeScript.
And voilà. The customQuery argument is now optional but not forced by the interface. Also thanks to that you’ll recognize to which composable you can pass the custom query.
Done.
Usage of function overloading with a custom query is a great example of how, along with TypeScript, we can define flexible and scalable logic in our application. The end result will always be tailored to our needs. However, we can use such logic regardless of the data source!
As we believe in the MACH-oriented approach , we are determined to deliver technology that serves business purposes, and that is another argument proving the potential of Alokai in that terms.
Hope you'll like it.
Cheers, Łukasz.