Create a Complex Reactor Script

When a use-case or integration goes deeper than a couple of simple functions, it's a good practice to build a Reactor script as a larger bundle of separate pieces of functionality instead of one large file. This way, you can use local development tools and an integrated development environment (IDE) to boost your productivity. When a new version needs to be tested, create a .zip file containing the project files (minus node_modules) and upload it through the Dashboard.

This page walks you through creating a more complex Reactor script that supports multiple action types and simulates a fictitious use-case scenario.


A Simple Supply Chain

In this walkthrough, you'll implement a simple Supply Chain scenario to apply some programmable logic to inventory movements. Imagine a company that makes 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. This example attaches the intended market region and additional information after the item has been sold.

A set of custom action types represents each phase of the product’s journey. The script itself uses the 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 goes through is as follows:

  • Item is made. Details of the intended country market are recorded on the Thng.
  • 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 it 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.

This is codified in the following action type table, along with the effects on the Thng (recorded in its customFields).

EventAction TypeEffects
Item manufactured_createdSet intended destination country
Item moved to new location_movedConfirm action location is within intended country; if not, create _outOfRegion action
Item registered for export to new region_exportedUpdate destination country for new market area
Item sold to end consumer_soldRemove destination and record details of sale
Item is found to be out of region after it has moved_outOfRegionCreate an action of this type

Before continuing, be 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 resides within this application.

To begin, the script contains only the Reactor event handler, which can be initially used to log any actions created:

const onActionCreated = async (event) => {
  logger.info('Action created!');
  done();
};

To begin creating the solution, implement a single function that performs a lookup for each action type it handles. Delegate it 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 are 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 uses. After each of these have been created, a reference to them is 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 has an action of type _created to record this event in its lifecycle. This action contains metadata to set up the region rules in its customFields. Such an action looks like the example 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 is created on the Thng. The location of this action can be compared to the destinationCountry in the Thng's customFields to determine whether it is in the correct region.

If it's not, an extra _outOfRegion action can 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 an 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 confirms its location to its intended region (you will implement this below). 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 needs to be updated to prevent false positive alerts. To do this, the _exported action type changes the destinationCountry on the Thng’s customFields so that it's in the right region the next time it's 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();
};

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 is ./actions/sold.js, which handles all actions of type _sold. These actions contain data about the sale (such as discount codes applied, location, special offers, user information, and so on) in the action customFields. We want to store this information the Thng, and simultaneously remove the now-redundant supply chain data.

This is 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 to be uploaded and built. The result is ‘OK. The reactor script is up and running.’

Next, create a Thng to be shipped across the world. Ensure it is correctly scoped to the project you're 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's been stolen. For details, see the Locations page in the API Reference.
  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 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, be 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

This walkthrough provides an understanding of a more complex 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 provide some insights into stock movements and alerts on those that stray outside their intended market region.

📘

Complete Example

See the examples repository on GitHub for a full copy of this Reactor script.

As the size and complexity of a Reactor script increases, it can become difficult to debug. The next section covers strategies that can help in debugging and testing efforts.