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

_created

Set intended destination country

Item moved to new location

_moved

Check action location is within intended country, if not create _outOfRegion action

Item registered for export to new region

_exported

Update destination country for new market area

Item sold to end consumer

_sold

Remove destination and record details of sale

Item is found to be out of region after is has moved

_outOfRegion

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:

  1. Open the containing directory and combine main.js, package.json, and the actions directory into a single .zip file. (These three files must be found at the root of the compressed folder in .zip format)
  2. Log into the EVRYTHNG Dashboard.
  3. Go to the application’s resource page and click the pencil icon in the top-right corner of the ‘Reactor’ pane.
  4. 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:

  1. _created with customFields.destinationCountry set to the country of manufacture, simulating item manufacture.
  2. _moved, simulating movement within the manufacturing country.
  3. _moved with location 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.
  4. _exported with customFields.destinationCountry set to a new country the item could be exported to.
  5. _moved with location set to some coordinates inside the new country, simulating a move to the retailer.
  6. _sold with customFields.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.