Runtime - Browser
Since the rules engine is a pure JavaScript library, it can be used in the browser. Just like you installed the library in Node.js, you can install it in the browser using npm:
npm install @euxdt/node-rules-engine
In the previous example, we loaded the configuration from a JSON file. Since the config api gives you a configurable callback to load the configuration, you can load the configuration from any source you want. For example, you can load the configuration from a file on the server, via a URL, or an API call.
Sample Application
The sample application is a simple React application that demonstrates the Rules Engine, Feature Flags and Configuration Management capabilities in the browser.
Get the Code
git clone https://github.com/flexicious/javascript-rules-engine
Install Dependencies
npm install
Run the Application
npm start
Once the application is running, you can open the browser and navigate to http://localhost:3000. You can then click on the dropdown in the top right corner to select different users and see how the feature flags and configuration change based on the user's attributes.
Configuration
This sample application comes pre-configured with a config file located at src/config.json. This file contains the LambdaGenie configuration. You can update this file with your own LambdaGenie config that you created in the previous section.
Loading the Configuration
As we covered in the previous sections, the configuration is loaded using a callback. The callback is defined in the src/config.js file. The callback is defined as follows:
import { ConfigJson } from "@euxdt/node-rules-engine";
import { getNodeJsConfigApi, ConfigApi } from "@euxdt/node-rules-engine";
import configJson from "../config.json";
// This is the config json that we would normally get from the lambda genie console
// we are just hard coding it here for the sake of the demo
// In a real world scenario, this would be loaded from the url or an api
export const loadConfigApi = async (lambdaName:string):Promise<ConfigApi> => {
const configApi = await getNodeJsConfigApi(
{
lambdaName,
cacheDurationSeconds: 60,
loadConfig: async (lastRefreshed?:Date, existingConfig?:ConfigJson) => {
//this is called every time the cache expires
//you can call out to an api here. It will only be called once every minute
console.log("Loading config");
const result = (configJson);
return result as unknown as ConfigJson;
},
log: (level, message, extra) => {
console.log(level, message, extra);
}
}
);
return configApi;
};
There is a word wrap button in the top right corner of the code blocks. If you are having trouble reading the code, click the word wrap button to make the code easier to read.
The callback is called every time the configuration is loaded. In this case, we are loading the configuration from a JSON file. In a real world scenario, you would load the configuration from a URL or an API call. The callback is passed the last time the configuration was refreshed, and the existing configuration. You can use this information to determine if you need to load the configuration from the server. For example, you could have 2 endpoints on the server, one that returns the last modified time of the config, and another that returns the config. If the last modified time is greater than the last time the configuration was refreshed, then you can load the configuration from the server. This way, you only load the configuration from the server when it has changed.
Using the Configuration
We have implemented a mock server that emulates a backend service. The mock server is located at src/mockServer/products.ts.
import { executeRule } from "@euxdt/node-rules-engine";
import { CONFIG } from "../shared/config-bindings";
import { loadConfigApi } from "../shared/config-utils";
import { Product, ProductApiResponse, SlotNames } from "../shared/types";
import { PRODUCTS } from "../shared/products";
export interface User {
username: string;
birthdate: string;
gender: string;
email: string;
}
export const getProducts = async (
user:User
) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
//This would come from environment variables, but for the sake of the demo, we'll hard code it
const environment = "prd";
//Load the config
const configApi = await loadConfigApi("GET_PRODUCTS");
//Get the slot names from the dynamic config we defined in the lambda genie console
const slotNames = JSON.parse(await configApi.getConfigValue(CONFIG.LAMBDA_CONFIGS.GET_PRODUCTS.SLOT_NAMES, environment)) as SlotNames;
//Default the slot names to some values if they aren't defined in the dynamic config
const slot1 = [slotNames.slot1] || ["Home & Kitchen"];
const slot2 = [slotNames.slot2] || ["Clothing, Shoes & Jewelry"];
const slot3 = [slotNames.slot3] || ["Learning & Education"];
const slot4 = [slotNames.slot4] || ["Hobbies"];
const maxProducts = slotNames.maxProducts || 4;
//Get the featured products from the dynamic config we defined in the lambda genie console
const featuredProducts = JSON.parse(await configApi.getConfigValue(CONFIG.LAMBDA_CONFIGS.GET_PRODUCTS.FEATURED_PRODUCTS, environment)) as Product[];
//Get the slot 2 rules from the rule set we defined in the lambda genie console, which allow
//us to dynamically change the slot 2 category based on the user's age and gender
const slot2Rules = configApi.configJson.ruleSets.find((ruleSet) => ruleSet.name === CONFIG.RULE_SETS.HOME_PAGE_PERSONALIZATION);
const userInfo = {
...user,
name: user.username,
age: user.birthdate ? Math.floor((new Date().getFullYear() - new Date(user.birthdate).getFullYear())) : 25
};
if (slot2Rules) {
//Execute the rules and get the result
const slot2RuleResult = executeRule(slot2Rules,
configApi.configJson.predefinedLists, userInfo, environment);
if (slot2RuleResult && slot2RuleResult.result) {
//this will be one of Men, Women, Boys, or Girls [As defined in the rule set in the lambda genie console]
slot2.push(String(slot2RuleResult.result));
}
}
//Now, lets get the next gen feature flag from the rule set we defined in the lambda genie console
let nextGenFeature = false;
const featureFlagRules = configApi.configJson.ruleSets.find((ruleSet) => ruleSet.name === CONFIG.RULE_SETS.NEXT_GEN_FEATURE);
if (featureFlagRules) {
//Execute the rules and get the result
const featureFlagRuleResult = executeRule(featureFlagRules,
configApi.configJson.predefinedLists, userInfo, environment);
nextGenFeature = featureFlagRuleResult.result ? true : false;
console.log("featureFlagRuleResult", featureFlagRuleResult);
}
//For now, we are just loading the products from a local file, but this could be replaced with a call to a database or an API
const getProducts = (slot: string[]) => {
return PRODUCTS.filter((product) => {
return slot.every((category) => {
return product.categories.includes(category);
});
}).slice(0, maxProducts);
};
//Get the products for each slot
const slot1Products = getProducts(slot1);
const slot2Products = getProducts(slot2);
const slot3Products = getProducts(slot3);
const slot4Products = getProducts(slot4);
//Return the full result
const response:ProductApiResponse ={
slot1: slot1[0],
slot2: slot2[0],
slot3: slot3[0],
slot4: slot4[0],
slot1Products,
slot2Products,
slot3Products,
slot4Products,
featuredProducts,
nextGenFeature
}
return response;
}
There is a word wrap button in the top right corner of the code blocks. If you are having trouble reading the code, click the word wrap button to make the code easier to read.
As you can see, we are loading the configuration from the config API, and then using the configuration to determine the slot names, the featured products, and the next gen feature flag. As you change the user being passed in via the dropdown in the top right corner, we execute the rules engine with the user's information, and then use the result to determine the slot 2 category, as well as the next gen feature flag. Since this is a demo, we're just loading the products from a local file, but in a real world scenario, you would load the products from a database or an API.
Client Side Specific Concerns
Since the rules engine is being executed on the client side, there are a few caveats. You would not use the rules engine to determine if a user is authorized to access a page, or to determine if a user is allowed to perform an action. The rules engine is only meant to be used to determine what content to show the user, or what features to enable for the user. If you are using the rules engine for authorization purposes, you would need to do this on the server side, and then pass the result to the client side, like we did with the previous example in Next.js.
Conclusion
In this tutorial, we have shown you how to use the Lambda Genie configuration API to dynamically change the configuration of your application. We have also shown you how to use the Lambda Genie rules engine to dynamically change the content of your application based on the user's information. Finally, we covered how to use the Lambda Genie feature flags to dynamically enable or disable features in your application. All of this is happening completely in the browser, with the rules being executed on the client side. The intent was to show you that you can use lambda genie in a variety of ways, and that it is not limited to just server side configuration.
We hope you enjoyed this tutorial, and we look forward to seeing what you build with Lambda Genie!