Create a Complex Reactor Script
When a use-case or integration goes deeper than a couple of simple functions, it is a good practice to architect a Reactor script as a larger bundle of many files of small separate pieces of functionality, instead of one overly large file. With this approach comes opportunities to use local development tools and an IDE to boost productivity. When a new version needs to be tested, simply create a .zip
file of the project files (minus node_modules
) and upload through the Dashboard.
This page will walk you through the process of creating an example of a more complex Reactor script that has functionality for multiple action types and simulates a fictitious use-case scenario.
A Simple Supply Chain
For this walkthrough, we will be implementing a simple Supply Chain scenario to apply some programmable logic to inventory movements. Imagine a supplier that manufactures t-shirts, and ships them internationally. We can use the Reactor to attach different information to the Thng that represents each item at each stage of the supply chain. In this example, this will be the intended market region, and additional information once it has been sold to a consumer.
A set of custom action types will represent each phase of the product’s journey. The script itself will use the previously discussed multi-file bundle pattern to keep the behavior of each action type’s reaction in a separate file to promote maintainability and stability if errors occur.
A summary of the journey the example Thng will go through is as follows:
- Item is manufactured - details of the intended country market are recorded on it.
- Item is moved to a new location within the region. If it is outside the intended country market area, create an action of type
_outOfRegion
to record that is has strayed. This can happen multiple times as the item is transported. - Item is exported to a new country market area, allowing it to move there as expected.
- Item is finally sold to an end consumer. The supply chain information is removed, and details of the sale are recorded on it.
This is codified in the following action type table, along with the effects on the Thng (recorded in its customFields
).
Event | Action Type | Effects |
---|---|---|
Item manufactured |
| Set intended destination country |
Item moved to new location |
| Check action location is within intended country, if not create |
Item registered for export to new region |
| Update destination country for new market area |
Item sold to end consumer |
| Remove destination and record details of sale |
Item is found to be out of region after is has moved |
| Create an action of this type |
Before continuing, make sure to create each of these custom action types, either through the Dashboard or the API, in your own account.
Beginning the Script
Create a project and application in the EVRYTHNG Dashboard or via the API, and scope the above action types to that project, if you haven’t already. The Reactor script for this example use-case will reside within this application.
To begin with, the script will contain only the Reactor event handler itself, which can be initially used to log any actions created:
const onActionCreated = async (event) => {
logger.info('Action created!');
done();
};
To begin creating the solution we will implement a single function that performs a lookup for each action type it handles, and delegate to a separate file for each one. Each of these action type handler files export a single function that returns a promise, thus allowing us to ensure that done()
is called, and errors are logged to the Platform for analysis.
Add the following to begin the implementation:
const HANDLERS = {
// File handler references will go here later on
};
const handleAction = (action) => {
const handler = HANDLERS[action.type];
if(!handler) {
throw new Error(`No handler for action type ${action.type}`);
}
return handler(action);
};
const onActionCreated = async (event) => {
try {
await handleAction(event.action);
} catch (e) {
logger.error(e.message || e.errors[0]);
}
done();
};
For each action type we are expecting to handle, we can simply add an item to HANDLERS
, and create a corresponding file in an directory called actions
adjacent to main.js
in the bundle. For example, the scans
action type handler file would live in ./actions/scans.js
and simply log the action and return the required Promise:
module.exports = async (action) => {
// Log the action
logger.info(`New action:\n${JSON.stringify(action)}`);
};
And then the handler file is added to HANDLERS
in main.js
:
const HANDLERS = {
scans: require('./actions/scans'),
};
To add the remaining action types that will be used throughout this supply chain scenario, we simply repeat this process to add more action type handlers. The code is kept tidy by separating concerns to different files, and making sure promise chains ensure done()
is called and errors are caught properly.
Implementing Multiple Action Types
With the skeleton of the Reactor script set up, it’s time to create action type handler files for each of the action types the scenario will use. After each of these have been created, a reference to them will be added in main.js
to enable it to be run for actions of that particular type.
Item Manufactured (1/4)
When an item is created, it will have an action of type _created
created on it to record this event in its lifecycle. This action will contain metadata to set up the region rules in its customFields
. Such an action will look like below (the destination in this case is France):
{
"type": "_created",
"thng": "UHTSkVPMe6PrtNwaaDWChh6d",
"customFields": {
"destinationCountry": "FR"
}
}
When this type of action is created, we want to record this metadata on the Thng so that it can be referenced later when its position is checked for a _moved
type of action. Create ./actions/created.js
, and enter the following code to perform this update on the Thng:
module.exports = async (action) => {
// Log the event
logger.info(`${action.thng} > ${action.customFields.destinationCountry}`);
// Update the Thng's customFields
const payload = { customFields: action.customFields };
return app.thng(action.thng).update(payload);
};
Back in main.js
, add this new action type handler file in the HANDLERS
object, so that it can be run for the appropriate type of action in onActionCreated()
:
const HANDLERS = {
_created: require('./actions/created'),
};
Item Moved to New Location (2/4)
When an item is moved to a new location, the _moved
action will be created on the Thng. The location of this action should be compared to the destinationCountry
in the customFields
on the Thng to determine whether it is in the correct region.
If not, an extra _outOfRegion
action should be created to record this event. An action of this type is simple (the location of the action is automatically determined from the location of the request creator):
{
"type": "_moved",
"thng": "UHTSkVPMe6PrtNwaaDWChh6d"
}
Create a new action type handler file ./actions/moved.js
and begin with the basic structure of the handler - a single exported function that reads the Thng referenced by the action, and then checks its location to its intended region (we will implement this soon). Once again, this function returns a Promise to retain the value the pattern adds.
module.exports = async (action) => {
// Read the Thng, then check its region
const thng = await app.thng(action.thng).read();
return checkRegion(thng, action);
};
The implementation of checkRegion()
is shown below. It is a simple check of the Thng’s recorded destinationCountry
against the context.countryCode
of the action that recorded the movement:
const checkRegion = async (thng, action) => {
const newLocation = action.context.countryCode;
logger.info(`Moved ${thng.id} to ${newLocation}`);
// If in intended country, stop here
if (thng.customFields.destinationCountry === newLocation) {
logger.info('In expected region');
return;
}
// If not in intended country, create alert action
logger.info('Out of region!');
const payload = {
thng: action.thng,
customFields: { newLocation },
};
const newAction = await app.action('_outOfRegion').create(payload);
logger.info(JSON.stringify(newAction));
};
As with the last action type handler file, remember to add the reference in main.js
:
const HANDLERS = {
_created: require('./actions/created'),
_moved: require('./actions/moved'),
};
Item Exported to New Region (3/4)
If an item is to be moved to a new intended country market area, the item will need to also be updated to prevent false positive alerts. To do this, the _exported
action type will be used to change the destinationCountry
on the Thng’s customFields
so that is it is in the right region the next time it is moved with a _moved
action. Begin the implementation in ./actions/exported.js
:
module.exports = async (action) => {
// Read the Thng, then update it
const thng = await app.thng(action.thng).read();
return updateThng(thng, action);
};
This follows the same pattern as the last handler file, which means that the majority of the logic is separately implemented as a function that returns a promise:
const updateThng = (thng, action) => {
// Update the Thng's destination to that in the action
thng.customFields.destinationCountry = action.customFields.destinationCountry;
logger.info(`Exported ${action.thng} to ${thng.customFields.destinationCountry}`);
return thng.update();
};
Once again, remember to add the reference for this action type handler file in main.js
:
const HANDLERS = {
_created: require('./actions/created'),
_moved: require('./actions/moved'),
_exported: require('./actions/exported'),
};
Item Sold to Consumer (4/4)
Finally, the last action type handler file in this scenarios will be ./actions/sold.js
, and will handle all actions of type _sold
. These will contain data about the sale (such as discount codes applied, location, special offers, user information, etc.) in the action customFields
. We want to store this information the Thng, and at the same time remove the now-redundant supply chain data.
This is quite simple to do, and the implementation is as follows:
module.exports = async (action) => {
logger.info(`Sold ${action.thng}: ${JSON.stringify(action.customFields)}`);
// Update the Thng's customFields to those in the action
const payload = { customFields: action.customFields };
return app.thng(action.thng).update(payload);
};
Add the final reference to the last action type handler file in main.js
:
const HANDLERS = {
_created: require('./actions/created'),
_moved: require('./actions/moved'),
_exported: require('./actions/exported'),
_sold: require('./actions/sold'),
};
With all the relevant action type handlers in place, it’s time to begin testing with some actions!
Testing the Scenario
Assuming you have followed along and have a project to test, now is the time to upload the collection of script files to the Platform:
- Open the containing directory and combine
main.js
,package.json
, and theactions
directory into a single.zip
file. (These three files must be found at the root of the compressed folder in .zip format) - Log into the EVRYTHNG Dashboard.
- Go to the application’s resource page and click the pencil icon in the top-right corner of the ‘Reactor’ pane.
- Upload the
.zip
file and wait for the script the be uploaded and built. You should be rewarded with ‘OK. The reactor script is up and running.’
Next, create a Thng that will be shipped across the world. Ensure it is correctly scoped to the project you are using to contain the Reactor script application. One easy way to do this is to use the application’s Trusted Application API Key, available from its application resource page in the Dashboard. Alternatively you can specify the project
query parameter with the project ID.
curl -H "Content-Type: application/json" \
-H "Authorization: $TRUSTED_APPLICATION_API_KEY" \
-X POST 'https://api.evrythng.com/thngs' \
-d '{
"name": "Shipped T-Shirt",
"description": "A t-shirt that will travel to its intended region",
"tags": ["Walkthrough"]
}'
Testing Manually
With the Thng created, create some actions to simulate a single item’s journey through this simple supply chain, and observe the logs in the Reactor pane of the application resource page:
_created
withcustomFields.destinationCountry
set to the country of manufacture, simulating item manufacture._moved
, simulating movement within the manufacturing country._moved
withlocation
set to some coordinates outside the country to simulate a misguided item or one that has been stolen. See the Locations page in the API Reference for details._exported
withcustomFields.destinationCountry
set to a new country the item could be exported to._moved
withlocation
set to some coordinates inside the new country, simulating a move to the retailer._sold
withcustomFields.price
or similar set, simulating the item being sold to an end consumer.
After these have been done, the Reactor logs and action history should tell a story about the item’s journey, including the warning that it might have strayed from its intended country.
Testing with a Script
Alternatively, use the following Node.js script to automate creating realistic actions for the Thng’s supply chain journey. Before running, make sure to install the dependencies and export TRUSTED_APPLICATION_API_KEY
and THNG_ID
with the appropriate values in your terminal session.
const async = require('async');
const EVT = require('evrythng-extended');
const { TRUSTED_APPLICATION_API_KEY, THNG_ID } = process.env;
const daysAgo = n => new Date().getTime() - (n * 1000 * 60 * 60 * 24);
const geoJson = coordinates => ({ position: { type: 'Point', coordinates } });
const LOCATIONS = {
FACTORY: geoJson([ -0.157196, 51.494302 ]),
EXPORT_WAREHOUSE: geoJson([ 1.304892, 51.117160 ]),
IMPORT_WAREHOUSE: geoJson([ 1.864851, 50.960105 ]),
RETAILER_WAREHOUSE: geoJson([ 2.275774, 48.935406 ]),
RETAILER_SHOP: geoJson([ 2.364685, 48.861690 ])
};
const ACTIONS = [
{ type: '_created', thng: THNG_ID, timestamp: daysAgo(28),
location: LOCATIONS.FACTORY, customFields: { destinationCountry: 'GB' } },
{ type: '_moved', thng: THNG_ID, timestamp: daysAgo(25),
location: LOCATIONS.EXPORT_WAREHOUSE },
{ type: '_exported', thng: THNG_ID, timestamp: daysAgo(20),
location: LOCATIONS.EXPORT_WAREHOUSE, customFields: { destinationCountry: 'FR' } },
{ type: '_moved', thng: THNG_ID, timestamp: daysAgo(14),
location: LOCATIONS.IMPORT_WAREHOUSE },
{ type: '_moved', thng: THNG_ID, timestamp: daysAgo(7),
location: LOCATIONS.RETAILER_WAREHOUSE },
{ type: '_moved', thng: THNG_ID, timestamp: daysAgo(3),
location: LOCATIONS.RETAILER_SHOP },
{ type: '_sold', thng: THNG_ID, timestamp: daysAgo(0),
location: LOCATIONS.RETAILER_SHOP, customFields: { price: 7.50, discount: 0.10 } }
];
const main = () => {
const trustedApp = new EVT.TrustedApp(TRUSTED_APPLICATION_API_KEY);
async.eachSeries(ACTIONS, (action, next) => {
return trustedApp.action(action.type).create(action)
.then(console.log)
.then(() => setTimeout(next, 3000));
}, (err, results) => {
if (err) {
console.log(err);
}
});
};
main();
Conclusion
After following this walkthrough you should have an understanding of one example of a more involved type of Reactor script that deals with multiple action types to implement a simple supply chain implementation. In the real world, tracking the counts and locations of the various action types would lead to some insights into stock movements and alerts on those that strayed outside their intended market region.
Complete Example
See the
examples
repository on GitHub for a full copy of this complex Reactor script.
As the size and complexity of a Reactor script increases, it can become difficult to debug if you don’t get it right first time. In the next section we will look at some strategies that can help in debugging and testing efforts.
Updated about 2 months ago