Alokai comes with a built-in method of communicating with the commercetools platform - composables. However, we understand that users may have specific needs for their projects. That's why we've designed our composables to allow custom queries that cover all our users' needs. If you've ever wondered how to create and use a custom query with Alokai and the commercetools integration, this article is for you! So, let's get started and create something cool together using your favorite IDE.
Prerequisites
For this article, we assume that you already have your Alokai and commercetools projects up and running.
If that's not the case, you can always contact us on Discord for help, or contact the Alokai Sales Team to help you get started.
The Default Query
Before we start writing the custom query, let’s first prepare our example page. For this article, we will use the Product.vue page that comes out of the box.
If you look at the source code for this page, you can see that we are using useProduct composable to fetch the data for the product. We retrieve the information about our product from the URL:
setup() {
//...
const route = useRoute();
const { products, search, error: productError, loading } = useProduct('products');
const fetchProducts = async () => {
try {
//...
await search({ skus: [route.value.params.sku] }); // useProduct composable search method
//...
};
When the search method call is done, it populates products with the fetched data. Let’s investigate the products state and see what is being stored there.
(17) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, __ob__: Observer]
0:
attributesRaw: Array(16)
availability: Object
id: 1
images: Array(2)
price: Object
sku: "M0E20000000E59L"
__typename: "ProductVariant"
_categories: Array(2)
_categoriesRef: Array(2)
_description: "Blue jeans that are high-quality, durable, and stylish."
_id: "891c95f8-7bf4-4945-9ab5-00906a5f76ba"
_key: undefined
_master: true
_name: "Jeans Closed blue"
_original: Object
_rating: Object
_slug: "closed-jeans-c3202059-blue"
The products state is populated with an array of objects for products that match the sku number.
However, the product object does not contain all the necessary information. All products have a taxCategory information that the default query does not fetch. What if we really need that information? The answer is a custom query.
Writing a Custom Query
We are mostly satisfied with all the information provided by the default query. For this article, we will extend the existing default query and add the taxCategory field to it, but in reality, you may need to write the query from scratch.
Create a folder called customQueries in the root of your project. If you already have one, you can simply open it. Inside this folder, create a new file named productTaxCategory.js. This is where we will write our new custom query.
// ./customQueries/productTaxCategory.js
// add taxCategory field on products query
module.exports = `
query products(
// query args
) {
products(
where: $where
sort: $sort
limit: $limit
offset: $offset
skus: $skus
) {
offset
count
total
results {
id
taxCategory {
name
rates {
amount
country
includedInPrice
subRates {
name
amount
}
}
}
reviewRatingStatistics {
averageRating,
ratingsDistribution,
count
}
masterData {
// rest of the query
}
}
}
`;
We have omitted the rest of the query and other details to make it shorter, otherwise, it would be 150 lines long. But, you can find this exact query here - GitHub Gist .
Okay, great, the new query is ready, but how are we supposed to use it? 🤔
First, let's create a function that will call our query. This is a simple function, but it's important to follow the pattern.
If there is no index.js file in the same folder, create one. Then, import the custom query that you have just created and write a function following this pattern:
const productTaxCategory = require('./productTaxCategory');
module.exports = {
productTaxCategory: ({ query, variables, metadata }) => {
console.log('productTaxCategory variables: ', variables);
console.log('productTaxCategory metadata: ', metadata);
return { query: productTaxCategory, variables };
}
};
So, what are we doing here? We are exporting a function called productTaxCategory that accepts an argument object. For the sake of this article, we added two console.log statements to show that the query is actually being sent. This function then needs to be provided to the middleware. To do that, open middleware.config.js in the root of the project.
const { ctCustomQueries } = require('./extensions/ct');
require('dotenv').config();
const stringToArrayValue = (val, separator = ',') => {
return typeof val === 'string' ? val.split(separator) : [];
}
module.exports = {
integrations: {
ct: {
location: '@vsf-enterprise/commercetools-api/server',
configuration: {
api: {
uri: process.env.VSF_API_URI,
apiHost: process.env.VSF_API_HOST,
authHost: process.env.VSF_API_AUTH_HOST,
projectKey: process.env.VSF_PROJECT_KEY,
clientId: process.env.VSF_API_CLIENT_ID,
clientSecret: process.env.VSF_API_CLIENT_SECRET,
scopes: stringToArrayValue(process.env.VSF_API_SCOPES)
},
serverApi: {
clientId: process.env.VSF_SERVER_API_CLIENT_ID,
clientSecret: process.env.VSF_SERVER_API_CLIENT_SECRET,
scopes: stringToArrayValue(process.env.VSF_SERVER_API_SCOPES),
operations: stringToArrayValue(process.env.VSF_SERVER_API_OPERATIONS),
},
currency: 'USD',
country: 'US',
languageMap: {
en: ['en', 'de'],
de: ['de', 'en']
}
},
customQueries: ctCustomQueries
}
};
As you can see, we already have a ctCusomQueries provided. We need to import new customQueries we have created and add them here:
const customQueries = require('./customQueries');customQueries: {
...ctCustomQueries,
...customQueries
}
Since we have made changes to the middleware.config.js file, please restart your application.
Great! We have registered a new custom query in our middleware, now, we need to use it in the application!
Using Custom Queries
Let's go back to the Product.vue file and update the search method to use a new custom query. Since the query is already registered in the middleware, we don't need to import anything. All we have to do is provide the query name as a parameter for the search method:
await search({ skus: [route.value.params.sku], customQuery: { products: 'productTaxCategory' } });
Note that in order for the custom query to work, we must provide a proper key name for the customQuery object. This name should be the same as what we used in the GraphQL query itself.
//...rest of GraphQL query
products(
where: $where
sort: $sort
limit: $limit
offset: $offset
skus: $skus
)
//...rest of GraphQL query
// products as a key for the customQuery object
customQuery: { products: 'productTaxCategory' }
When you save and refresh the product page, you will receive an error. However, don't worry, this is expected behavior. The issue could be with the custom query.
If you look at the terminal, you will notice that the console.log printed the result.
productTaxCategory variables: {
where: null,
skus: [ 'M0E20000000E59L' ],
limit: undefined,
offset: undefined,
locale: 'en',
acceptLanguage: [ 'en', 'de' ],
currency: 'USD',
country: 'US'
}
productTaxCategory metadata: {}
So, this is not the issue! What is the issue then?
To better understand what is wrong, let's dive deeper into the internals of how the useProduct composable works. First, let's start with the search method itself.
async function search(searchParams) {
Logger.debug(`useProduct/${id}/search`, searchParams);
try {
loading.value = true;
products.value = await useProductMethods.productsSearch(context, searchParams);
error.value.search = null;
} catch (err) {
error.value.search = err;
Logger.error(`useProduct/${id}/search`, err);
} finally {
loading.value = false;
}
}
The search method accepts a searchParams object as an argument and calls useProductMethods.productsSearch internally with both the context and searchParams objects. The productsSearch method performs the following actions:
productsSearch: async (context: Context, { customQuery, enhanceProduct, ...searchParams }): Promise => {
const apiSearchParams = {
...searchParams,
...mapPaginationParams(searchParams)
};
// @ts-ignore
const response = await context.$ct.api.getProduct(apiSearchParams, customQuery);
catchApiErrors(response as unknown as ErrorResponse);
if (customQuery) return response as any;
// @ts-ignore
const enhancedProductResponse = (enhanceProduct || originalEnhanceProduct)(response, context);
return (enhancedProductResponse.data as any)._variants;
}
- The code performs the following actions:
- Creates an apiSearchParams object based on the searchParams argument.
- Calls a getProduct API endpoint.
- Catches errors.
- If customQuery exists, returns the response as is without any data transformation.
- Otherwise, enhances and maps the data to return an array of products.
If the customQuery parameter is passed, the data does not go through the enhanceProduct process, and the raw response data from the commercetools GraphQL API is returned. This is likely the reason for any errors encountered. To diagnose, inspect the products state to see what data is received.
// console.log(products.value) - populated after customQuery usage
data: Object
products: Object
count: 1
offset: 0
results: Array(1)
0:
id: "891c95f8-7bf4-4945-9ab5-00906a5f76ba"
masterData: Object
reviewRatingStatistics: Object
taxCategory: Object
__typename: "Product"
total: 1
__typename: "ProductQueryResult"
loading: false
networkStatus: 7
As described above, our products state is populated with the raw response from the commercetools GraphQL API. However, the response contains what we initially wanted: a taxCategory object for the product!
To fix the error on a Product.vue page, you should manually map the data in the way you will consume it later in your application. This approach is relatively new and was implemented by Alokai a while ago. Our goal was to give you complete freedom to get the data and use it as you wish!
With great power comes great responsibility.
- Uncle Ben
Conclusions
Creating custom queries is a relatively simple process that doesn't take much time. It can be a little confusing when you do it for the first time, but once you understand how it works, all following queries will be a piece of cake!
Alokai covers a majority of use cases, but also allows users to customize almost everything and tailor the application to their needs.
If something wasn't clear, or you need help with custom queries in your application, feel free to join us on Discord and register for upcoming Workshops where we will cover even more advanced concepts for the commercetools integration with Alokai application!
If you are interested in learning more, please check our commercetools documentation and sign up for the Academy . When you are confident in your knowledge, you can take a commercetools integration quiz to obtain a VSF certificate.