Trool: a spreadsheet rule engine for NodeJS/TypeScript

Sean Maxwell
Level Up Coding
Published in
7 min readApr 1, 2019

--

Get the rules out of your code so non-engineers can make updates overtime

Trool makes it easy to update your program overtime!

Trool Introduction

Trool is a simple npm library for NodeJS which allows you to create rules for your program in a spreadsheet-style format. Engineering time for businesses can be expensive: you want to keep that to a minimum. The problem is your program may need lots of small updates over time depending on business needs. It’s a waste of money to call on programmers every time a small change needs to be made. Instead of hard-coding these small changes in your code, why not move them to a spreadsheet, so you or non-engineers on your team can quickly make the changes.

While Trool can be used directly with JavaScript (ES5 or ES6) in NodeJS, all the documentation is in TypeScript and you are encouraged to use that.

How it works

To use Trool, you just need to create the classes to be updated and the spreadsheet, pass them all to a single Trool method, and you’ll receive back all of the same objects but with updates made according to your rules. The spreadsheet works by checking attributes on the provided objects and calling methods on each depending on how conditions are met. If you want to reuse values in multiple places throughout the spreadsheet, you can create Imports which are simple key/value pair lists. Imports can be hard-coded in the spreadsheet or passed through NodeJS code. Here’s a sample of what a spreadsheet might look like.

Trool spreadsheet for setting ticket prices based on visitor age and party size

Installation

$ npm install --save trool// the library includes the typings files so you don't need to install @types/trool

Tutorial

Setup Trool

After you’ve installed Trool, import and instantiate it in your program. You can pass a boolean to the constructor depending on whether or not you want Trool to show any logging data while it runs. You should turn this on during development and leave it off for production.

import Trool from 'trool';

class PriceCalculator {

private trool: Trool;

constructor() {
this.trool = new Trool(true);
}
}

Fact

To use Trool, you must first understand the concept of a Fact. A fact is an instance-object or array of instance-objects that need to be updated. To create a fact, implement a TypeScript class and make sure that every attribute has getters and setters. If you look at cell 2A in the screenshot above, you can see that the content of the cell is Table: Visitors. Visitors is a fact made using an array of Visitor instance-objects.

After you’ve created your TypeScript classes, the facts must be passed to Trool in a fact-holder object. The fact-holder object is a JSON object with the key being the name of the fact to be accessed in the spreadsheet and the value being the array of instance-objects. So with Table: Visitors, we need to setup a fact-holder object with Visitors as the key and a single visitor or array of visitors as the value.

public calculate() {    const factsHolder = {
Visitors: [new Visitor(), new Visitor()]
}
}

Decision-Table

After setting up your facts, open up Excel, LibreOffice Calc, or some other spreadsheet tool of your choice. To update facts we need to setup a decision-table. A decision-table is a group of operations (columns) and rules (rows). If a rule passes, operations will be called to update the fact. If a fact is an array, the decision-table will loop through each instance-object and apply its logic to each value.

The first cell of the table specifies the fact and it must follow the exact format Table: "Name of the fact" or an error will be thrown. Next create at least one Condition column and one Action column. If all the conditions pass, the action will be executed. There’s no limit to the number of conditions and actions you implement, but you must specify one of each and all conditions must come before all actions.

In the second row of the decision-table, create the operations for the conditions and actions. A condition must be a boolean operation and in the format factAttribute operator $param. In other words, $param must be on the right-side of the operation. The attribute on the left side of the operation could be a regular method or a getter, but is has to be one of them and exist on the instance-object. If it does not, Trool will throw an error.

An action is the operation to be fired when all conditions evaluate to true. This operation can be a setter or a regular method on the fact. If using a regular method, multiple $param can be specified.

A Rule is a list of values to be passed to the condition and action operations. The first cell of the rule must specify a rule-name which can be any value but cannot be blank. On a decision-table, Trool will loop through the cells in each rule then move on to the next rule. The value in the cell will replace the $param value in the operation and the operation will execute. If a cell is blank for a condition, it will automatically evaluate to true. If all conditions are true for a rule, the actions which have values for that rule will execute. If a cell is blank though, that action-operation will not execute. A cell value must be something that can be evaluated with a comparison operator and it’s highly recommended that you stick with primitives and not use objects as values.

Let’s look at a simple decision-table with 1 condition, 1 action, and 2 rules (“Set Price — Regular” and “Set Price — Season”). Trool will iterate through the Tickets array, and first look at the getter for option and compare it to the cell values for each rule. In the first rule (Set Price — Regular), the table will see if each Ticket’s option attribute is set to “Regular”. If it is, the setter for price will be called and set the value 70. After “Set Price — Regular” finishes, the table will go on to “Set Price — Season”.

Decision-Table for setting default ticket prices

Thanks to Trool, if your ticket prices change periodically or you want to add new ticket options in the future, you don’t have to waste valuable engineering time!

Imports

Suppose for a moment there’s a specific value that needs to be used in multiple tables and you need let’s say, and string value to always be spelled a certain way. For the ticket option “Season”, you may always want to make sure it’s spelled Season and not Seasonal or something. This is where imports come in handy. It’s generally a good idea to use string values through imports and not directly to prevent spelling errors. Imports can be implemented in the spreadsheet or passed through code. For larger applications, you may have imports created dynamically and need to pass them through the code. For simple apps, it’s recommended you keep your imports in the spreadsheet.

To create an import in the spreadsheet, start with the cell Import: “Import Name”. The next rows will be added as key/value pairs to the import until it reaches an empty line, table, or new import. You should keep your imports and decision-tables separated by blank lines for readability purposes.

Here’s an example of an import with 2 key/value pairs. With this import, instead of doing “Season” for the rule “Set Price — Season”, you could say TicketTypes.SEASON.

The TicketTypes Import

If you want to pass imports through your code, just like the facts, they must be passed via a holder object. The key must be the name of the import and the value the import itself. For the import TicketTypes that we just did in the spreadsheet, here is the equivalent doing the same thing in code:

public calculate(): void {    const factsHolder = {
Visitors: [new Visitor(), new Visitor()]
}
const importsHolder = {
TicketTypes: {
REGULAR: 'Regular',
SEASON: 'Season'
}
}
}

Using Trool

Once you’re done setting up the spreadsheet, make sure to export it as a .csv file. On the Trool instance, call the method applyRules(…), passing in the path to the csv file, the facts object, and optionally, any imports you decided to create in code and not in the spreadsheet. This method is asynchronous and returns a Promise containing an identical facts-holder object but with all the facts updated according to action-operations that have been called. Now your NodeJS program can proceed with all the updated values it needs.

public async calculate(): Promise<void> {    const factsHolder = {
Visitors: [new Visitor(), new Visitor()]
}
const importsHolder = {
TicketTypes: {
REGULAR: 'Regular',
SEASON: 'Season'
}
}
try {
const csvFilePath = path.join(__dirname,'filename.csv');
const updatedFacts = await this.trool.applyRules( csvFilePath, factsHolder, importsHolder);

} catch (err) {
console.log(err.message);
}
}

Conclusion

Hopefully Trool become of great use to you and drastically cuts down on your engineering time. There are some more rules for using Trool than what could be specified in one article. I highly encourage you to read the README via the GitHub link posted above to avoid errors when implementing your decision-tables.

Please star the GitHub repo if you found this article/library helpful :)

--

--