Programming Patterns

Every action has a mapping to a JavaScript file that contains a custom function. These JavaScript files are named according to the Type.Domain.Action.Occurrence syntax.

The files are grouped in folders organized by the domain the actions pertain to. To create the necessary action files and directory structure for the first time, use the recommended Kibo eCommerce Yeoman Generator scaffolding tool, as described in the Quickstart. The following image shows the http.storefront.pages.global.request.after action file located in the storefront domain folder.

The JavaScript files share the following basic structure:


module.exports = function(context, callback) {
	// Your custom code here
	callback();
};
		

When you code the custom function for an action, you have access to two arguments: context and callback.

context

The context argument provides the function access to relevant objects and methods that interface with Kibo eCommerce. The following table summarizes a few of the context objects and methods you can use to enhance the functions you develop. Refer to the reference content for full details about the context argument available for each action you are coding a custom function for, or log the context object to view its data.

apiContext Provides mostly read-only access to standard API variables, such as the tenant, site, master catalog, user claims, etc.
configuration Provides read-only access to configuration data that you set in the Action Management JSON editor.
exec Provides read and write access to functions that act on Kibo eCommerce objects. The methods accessible by exec vary depending on the action you are coding the function for.
get Provides read-only access to certain Kibo eCommerce objects. The objects accessible by get vary depending on the action you are coding the function for.

Examples:

var url = context.apiContext.baseUrl;
var customData = context.configuration.customField;
context.exec.removeItem(allocation.referenceItemId);
var cartItem = context.get.cartItem();

In addition, when you code for HTTP actions, you have access to context objects and methods related to the HTTP response and request, such as the following objects:

request Provides read access to the HTTP request made to the API endpoint the action accesses.
response Provides read and write access to the HTTP response made to the API endpoint the action accesses.

Variable Assignment

If you assign a variable to the value of a Kibo eCommerce resource, and then use an exec method to update the Kibo eCommerce resource, the variable in your JavaScript file does not automatically update with the new value of the Kibo eCommerce resource. For example, if you set a variable to the value of an order:

order = context.get.order();

...and then update the order using an exec method:

context.exec.setData("customField", "Amazing-Order");

...the Kibo eCommerce order resource updates with the new custom field but the order variable still retains the original value of the order resource at the time of the GET operation. If you want the variable to update to the latest value of the Kibo eCommerce order resource, you have to use the context.order.get method again.

callback

The callback argument enables non-blocking, asynchronous program flow by allowing you to propagate errors and other important information across function calls whenever needed. The callback argument follows the established JavaScript callback pattern: it takes an error as the first argument (or null if there is no error) and a result as the second argument (not required for Arc.js applications because you use context methods instead to perform state changes).

Important:  To ensure that your Arc.js function does not halt the Kibo eCommerce runtime or break storefront functionality, arrange callbacks so that they are the last pieces of code to execute within your function.

// Note: This code demonstrates using callbacks within a simplified if-else function structure. The recommended structure for Arc.js functions is the try catch statement, as explained in the next section.
module.exports = function(context, callback) {
	if (A) {
		// Do something.
		callback();
	}
	else if (B) {
		// Do something else.
		callback();
	}
	else {
		// Last option.
		callback();
	}				
};

Try Catch Error Handling

The try catch statement is useful for handling any errors that may occur in your Arc.js functions. It tells Kibo eCommerce to try to execute a block of code, and catch errors if they occur in the try block. Note, however, that the catch block does not detect errors that occur within a Promise chain. To catch these errors, use a .catch statement at the end of the Promise chain.

Important:  To ensure that your Arc.js function does not halt the Kibo eCommerce runtime or break storefront functionality, place callbacks within the try catch statement.If you are familiar with try catch statements, you may know that you can also employ a finally block to execute a final block of code, regardless of whether an error occurred in the try block. Counterintuitively, however, the finally block executes before Promise chains, and because you will likely employ Promises in your Arc.js functions, use of the finally block is generally not recommended.

The following code block demonstrates a high-level example of using the try catch statement within an action file:

module.exports = function(context, callback) {
	try {
		// For starters, run some cool code.
		var meaningfulVariable = 42;
		coolFunction(meaningfulVariable);
					
		// Run this cool Promise chain.
		someFunction().then(function(name) {
			if (something) {
				return somethingCool;
			} else {
				// Do something else.
				callback();
			}
		})
		.then(function() {
			callback();
		})
		.catch(function(err) {
			// Catch errors that occur in the Promise chain...
			console.error(err);
			callback();
		});
	}
	catch (error) {
		// Catch errors that occur in the try block, outside the Promise chain...
		console.error(error);
		callback();
	}
};

Interested in learning more about try catch statements? The Mozilla Developer Network has a good writeup on the topic.

Call a Third-Party Package from within Arc.js

One of the benefits of the Arc.js framework is that it allows you to leverage the thousands of NPM packages developed by the Node.js community. The following code blocks provide an example of loading the Needle HTTP client for use in Arc.js.

Note:  When integrating third-party packages, always load the smallest package possible. If the functionality you require is offered as a subset of the complete module, load the smaller module to minimize delays on your storefront.
  1. Install the package using npm by opening a command prompt in your project folder and entering npm install package name.

    Alternatively, you can list the SDK as a dependency in your package.json file by adding the following code to the file, where you can specify a specific version of the package using a caret followed by the version number or match any version by using an asterisk:
    "devDependencies": {
    	"needle": "*"
    }
  2. Require the package or a helper file that loads the package (shown) in one of your JavaScript files, and then use the package as needed.
    assets/src/domains/someDomain/someActionFile.js
    var serviceCall = require('../../util/needleRequest').makeSampleCall;
    module.exports = function (context, callback) {
    	var sampleData = "<value>" + context.request.url + "</value>";
    	serviceCall(sampleData)
    		.then(function (responseFromCall) {
    			console.log(responseFromCall);
    			callback();
    		})
    		.catch(function(err) {
    			console.error(err);
    			callback();
    		});
    }
    assets/src/util/needleRequest.js
    var needle = require('needle');
    exports.makeSampleCall = function (bodyStr) {
    	var promise = new Promise(function (resolve, reject) {
    		try {
    			needle.post('https://someServer.com',
    			{
    				headers: {
    					'Content-Type': 'text/xml'
    				},
    				body: bodyStr
    			}, function (error, response) {
    				if (!error && response.statusCode == 200) {
    					console.log('Inside needle request');
    					resolve(response);
    				} else {
    					reject(response.statusCode);
    				}
    			});
    		} catch (err) {
    			reject(console.error(err));    
    		}
    	});
      
    	return promise;
    };

Call the Kibo eCommerce Node.js SDK from within Arc.js

While the context argument provides each function access to pertinent methods and data, you may at times need access to the complete set of Kibo eCommerce API microservices. To access this broader array of functionality, call the Kibo eCommerce Node.js SDK from within Arc.js by completing the following steps:

  1. Install the SDK using npm by opening a command prompt in your project folder and entering npm install --save mozu-node-sdk.

    Alternatively, you can list the SDK as a dependency in your package.json file by adding the following code to the file, where you can specify a specific version of the SDK using a caret followed by the version number or match any version by using an asterisk:
    "devDependencies": {
    	"mozu-node-sdk": "*"
    }
  2. After installation, require and use the Kibo eCommerce Node.js SDK in your JavaScript files. The following code block is an example of retrieving an entity list within an action file.
    assets/src/domains/someDomain/someActionFile.js
    var entityListClientFactory = require('mozu-node-sdk/clients/platform/entityList');
    module.exports = function(context, callback) {
    	var entityListClient = entityListClientFactory(context);
    	entityListClient.context['user-claims'] = null; // Remove shopper user claims
    	// Run calls...
    	entityListClient.getEntities({
    		entityListFullName: 'mylist@' + context.nameSpace
    	}).then(function(list) {
    		// Handle the response...
    		callback();
    	}).catch(callback); // Pass the callback to the error handler to pass errors to runtime
    }

Notes for Using the Kibo eCommerce Node.js SDK

Add or Remove User Claims

When an Arc.js action executes a custom function, it applies the user claims that belong to the user initiating the action. In some cases, user claims have sufficient permissions to execute your needed operations, such as manipulating items in a shopper's cart. In other cases, however, you may need API-level instead of user-level claims to execute your needed operations, such as when you want to query the API regarding a Kibo eCommerce resource. When you need more permissions, remove the user claims as follows:

var someResourceFactory = require('mozu-node-sdk/pathToResource');

module.exports = function(context, callback) {
	var someResource = someResourceFactory(context.apiContext);
	someResource.context["user-claims"] = null;
}

What would a real-world example look like? The following code block shows a function that adds a shopper's recent purchases as extended properties of the cart (you could then use these properties in your theme to display a list of recently purchased items to returning shoppers). Because this function is bound to the embedded.commerce.carts.addItem.after action, the user claims correspond to the permissions of a shopper who has just added an item to their cart. Given this information, the function requires:

assets/src/domains/commerce.carts/embedded.commerce.carts.addItem.after.js
//Require the components of the Kibo eCommerce Node.js SDK that you need. These "Factories" help you generate Kibo eCommerce resources with appropriate context.
//Each of these Factories will need to receive an apiContext object to gain authorization to make API requests.
var CartExtendedPropertiesResourceFactory = require('mozu-node-sdk/clients/commerce/carts/extendedProperty');
var OrderResourceFactory = require('mozu-node-sdk/clients/commerce/order');

module.exports = function(context, callback) {
	//Wrap functionality in a try/catch block. This prevents errors from stalling your Kibo eCommerce storefront.
	try {
		//The context.apiContext contains both app-claims and user-claims.
		//The user-claims allow for making API requests scoped to the user who initiated this Arc.js action (adding an item to the cart).
		//The app-claims allow for making API requests scoped beyond the permissions of a user and are used to affect data in the Kibo eCommerce workflow.
    	//Since Order API calls are scoped outside of the permissions that a user is allowed, you'll remove the user-claims from the Order Resource you create.
		//Note: The original context.apiContext object is not altered.
		var orderResource = OrderResourceFactory(context.apiContext);
		orderResource.context["user-claims"] = null;
    
		//Carts are scoped directly to the users who own them. 
		//You retain the user-claims so you can write extended properties on the cart, which belongs to the shopper.
		var cartExtendedPropertyResource = CartExtendedPropertiesResourceFactory(context.apiContext);

		var cart = context.get.cart();
		var productCodeString = '';
		var extendedProperty;
		var hasExistingProperty = (cart.extendedProperty && cart.extendedProperty.length > 0) ? true : false;

		orderResource.getOrders({ filter: 'userId eq ' + cart.userId })
			.then(function(orderCollection) {

			if (orderCollection.totalCount > 0) {
				//console.log(orderCollection.totalCount);
				var reduceHandler = function(previousOrderItem, currentOrderItem) {
					productCodeString += previousOrderItem.product.productCode + "," + currentOrderItem.product.productCode + ',';
				};
				for (var i = 0; i < context.configuration.orderHistoryNumberOfOrders; i++) {
					orderCollection.items[i].items.reduce(reduceHandler);
				}
				productCodeString = productCodeString.slice(0, productCodeString.lastIndexOf(','));

				if (productCodeString.length > 0) {
					extendedProperty = [{
						key: context.configuration.extendedPropertyKey,
						value: productCodeString
					}];

					if (hasExistingProperty) {
						cart.extendedProperties.forEach(function(property, index) {
							if (property.key === context.configuration.extendedPropertyKey) {
								hasExistingProperty = true;
							}
						});

						if (existingProperty) {
							return cartExtendedPropertyResource.deleteExtendedProperty({ key: context.configuration.extendedPropertyKey })
								.then(function() {
									return cartExtendedPropertyResource.addExtendedProperties({}, { body: extendedProperty });
								});
						}
				}
				return cartExtendedPropertyResource.addExtendedProperties({}, { body: extendedProperty });
			} else {
				callback();
			}
		} else {
			callback();
		}
	})
	.then(function() {
		callback();
	})
	//Use the .catch statement to handle errors in the Promise chain. 
	//Be sure to execute a callback in case of errors to refrain from halting Mozu storefront execution.
	.catch(function(err) {
		console.error(err);
		callback();
	});
	//Use a catch block to handle errors that occur in the try block, outside of Promise chains.
	//Be sure to execute a callback in case of errors to refrain from halting Mozu storefront execution.
	} catch (error) {
		console.error(error);
		callback();
	}
};

Send Custom Data to Your Theme

You can set custom data within storefront HTTP after actions, and then expose that data in your theme for use on storefront pages. On the Arc.js side, you set the custom data in the context.response.viewData object. On the theme side, you access the data from the viewData variable, where it is stored as a top-level JSON object.

Note:  The context.response.viewData object contains an object called model, which represents the model for the current page. While you can access and modify the model object through Arc.js, you cannot add custom data that doesn't already exist in the model object. You can only set custom data at the context.response.viewData level.

When you set custom data for your theme in an Arc.js action, you must:

Arc.js Action File Where You Set Data Theme Templates Where You Access the Data
http.storefront.pages.cart.after.js templates/pages/cart.hypr and any included templates
http.storefront.pages.checkout.after.js templates/pages/checkout.hypr and any included templates
http.storefront.pages.productDetails.after.js templates/pages/product.hypr and any included templates
http.storefront.pages.myAccount.after.js templates/pages/my-account.hypr and any included templates

Example

The following is an example of setting custom data in an Arc.js action file (remember, this must be an HTTP storefront after action):

assets/src/domains/storefront/http.storefront.pages.productDetails.after
module.exports = function(context, callback) {
	context.response.viewData.yourCustomField = "yourCustomValue";
	callback();
};

...which you can then access in an appropriate theme template:

templates/pages/product.hypr
...
{{ viewData.yourCustomField }} {# renders "yourCustomValue" #}
...

Securely Store and Access Sensitive Data

Arc.js allows you to access encrypted data on your tenant. You first post the sensitive data using the Kibo eCommerce API, which encrypts the data and stores it at the tenant level as a credential store entry. Once the data is on your tenant, only an Arc.js application tied to your developer account can decrypt the entry—this is for security reasons; not even the Kibo eCommerce API can decrypt the entry. The following sections provide more information about the process:

Store Sensitive Data Using the Kibo eCommerce API

To securely store data on your tenant, use the StoreCredentials operation to POST your data to the platform/extensions/credentialStore API resource in the form of a credential store entry, which consists of the following fields:

{
	"fullName": "name@namespace",
	"value": [
		{
			// Custom data
		}
	]
}
Field Name Description
fullName A name for your entry, in the form of name@namespace, where the namespace must match the namespace of your developer account.Note:  If the name you choose already exists within the namespace, then the POST operation overwrites the previous value.
value A JSON object that represents whatever sensitive information you want to encrypt.

Access Sensitive Data Using Arc.js

While the Kibo eCommerce API receives, encrypts, and stores each credential store entry, it does not support retrieving an entry. To decrypt and access a credential store entry, use the getCredential method available in the context object of an Arc.js function. The Arc.js application that executes the function must be created within your developer account—other applications will not have the required permissions to run the getCredential method within your namespace.

context.getCredential('name@namespace', function (err, result){
	// Access sensitive data within the result variable
};

For its first argument, the getCredential method takes the value of the fullName field of the credential store entry you want to access. For the second argument, the method takes a callback. After you decrypt data using the getCredential method, you can assign the result to a variable and access the sensitive data from the entry's value object. See the following example use case for additional details.

Example Use Case

Let's say you want to encrypt an API key for a third-party service, and then access that key in an Arc.js function. Here's how you would go about doing that:

  1. In the request body, POST the following credential store entry to the platform/extensions/credentialStore API resource: Tip:  Remember that the namespace you choose should match the developer account that will contain the application for decrypting the sensitive data.
    {
    	"fullName": "myCredentials@myNamespace",
    	"value": [
    		{
    			"accountName" : "myAccountName",
    			"apiKey": "123456789"
    		}
    	]
    }
  2. Create an Arc.js application within the same developer account namespace where you posted the credential entry.
  3. Within an Arc.js custom function, access the encrypted data:
    module.exports = function(context, callback) {
    	context.getCredential('myCredentials@myNamespace', function (err, result){
    		if(err){
    			callback(err);
    			return;
    		}
    							
    		console.log("My third-party account name is " + result.accountName);
    		console.log("My third-party API key is " + result.apiKey);
    	};
    	callback();
    };