Create a Basic Reactor Script

After seeing some complete examples of simple Reactor scripts, in this section we will walk through the process of creating one from scratch to achieve a specific purpose - tracking the locations and quantities of scans on Thngs made using their QR codes. This is a good example of manipulating related Platform resources in reaction to some other event, which is one of the most common use-cases of a Reactor script.

This script will take the form of the previously discussed single file pattern, since it will be quite short and self-contained. All the information required to carry out this task is included in each action, provided that a Thng was the target.

Two of the available event types will be used:

  • onActionCreated() - On each scan, count total scans and accumulate cities for the target Thng. Also add a tag to show it has been included in this scenario.
  • onScheduledEvent() - Find all tagged Thngs, and reset the counter properties at midnight each day.

Beginning the Script

The first steps to creating a new Reactor script is to create a project, and then an application inside that project. You can do this through the Dashboard or the API directly. In this walkthrough we will be editing and uploading the script through the application resource page in the Dashboard.

With the application in place, click the ‘Edit’ pencil in the top right corner of the ‘Reactor’ section of the application page, and enter the following to begin the script:

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

const onScheduledEvent = async (event) => {
  logger.info('Scheduled event!');
  done();
};

These will be the two event handler functions that will be called by the Platform in each applicable case. When a Thng’s QR code is scanned, onActionCreated() will be called. After a Reactor schedule is set up, onScheduledEvent() will be called at midnight. More about that later.


Tracking Scans

Starting with onActionCreated(), the first step is to read the full resource for the Thng that was scanned using the ID stored in the action, in order to read its tags. With the evrythng.js SDK included in Reactor, this is straightforward:

const onActionCreated = async (event) => {
  try {
    // Read the Thng
    const thng = await app.thng(event.action.thng).read();
    logger.info(JSON.stringify(thng));
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

At this point the script will simply read and print the Thng. The next step is to read the cities it has been scanned in (if any) from a scan_cities custom field, and add the one for this action if it is not already included:

const onActionCreated = async (event) => {
  try {
    // Read the Thng
    const thng = await app.thng(event.action.thng).read();

    // Add the city if it isn’t already there
    const scanCities = thng.properties.scan_cities || [];
    const { city } = event.action.context;
    if (!scanCities.includes(city)) {
      scanCities.push(city);
    }
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

The final step is to update the Thng resource with this new information, and at the same time add an item to its tags. Using evrythng.jspromises and Promise.all(), this is also easy to do. Thus the final form of onActionCreated() looks like this:

const onActionCreated = async (event) => {
  try {
    // Read the Thng
    const thng = await app.thng(event.action.thng).read();
    
    // Add the city if it isn’t already there
    const scanCities = thng.properties.scan_cities || [];
    const { city } = event.action.context;
    if (!scanCities.includes(city)) {
      scanCities.push(city);
    }

    // Update the properties and add a tag
    await Promise.all([
      thng.property().update([
        { key: 'scan_count',  value: (thng.properties.scan_count || 0) + 1 }, 
        { key: 'scan_cities', value: scanCities },
      ]),
      thng.update({ tags: ['Walkthrough'] })
    ]);
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

Once both the properties update promise and the tags update promise are both resolved, the final .then() is called and the script ends with a call to done() and a catch to catch any errors.


Resetting Scans

In this scenario we want to clear the total scans for all Thngs with the ‘Walkthrough’ tags at the start of each day. In a more complete integration, perhaps a total count would be transmitted to a third party, or even stored on the Thng for historical reasons. This is beyond the scope of this walkthrough, but still a good simple example of a useful Reactor script.

The Reactor scheduler API makes this easy, using a simple cron expression to invoke the onScheduledEvent() Reactor script handler at 00:00 on every day of the month.

Make the curl request to the API shown below to enable this schedule before setting up the script to handle the callbacks. Make sure to substitute your account Operator API Key (found on the Account Settings page in the Dashboard), and the appropriate project and application IDs created earlier.

curl -H "Content-Type: application/json" \
  -H "Authorization: $OPERATOR_API_KEY" \
  -X POST 'https://api.evrythng.com/projects/:projectId/applications/:applicationId/reactor/schedules' \
  -d '{ 
    "function": "onScheduledEvent", 
    "event": {}, 
    "cron": "0 0 0 * * ?", 
    "description": "Run every day at midnight", 
    "enabled": true 
  }'

To enable a callback every time this schedule occurs, implement onScheduledEvent() in the Reactor script, alongside the existing code. From the basic handler you added earlier, create an evrythng.js iterator to iterate through all Thngs in the application scope, and filter by only those that have the ‘Walkthrough’ tag:

const onScheduledEvent = await (event) => {
  try {
    // Get an iterator for all Thngs with 'Walkthrough' tag
    const params = { filter: 'tags=Walkthrough' };
    const iterator = app.thng().iterator({ params });
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

Using the EVT.Utils.forEachAsync function with the iterator, it is easy to reset all concerned Thngs to remove their scan count and the cities in which they were scanned. This is done by retrieving a filtered list of Thngs and mapping them to promises that can be executed together to perform the updates.

Once this is done, the final form of the onScheduledEvent() handler is as follows:

const onScheduledEvent = async (event) => {
  try {
    // Get an iterator for all Thngs with 'Walkthrough' tag
    const params = { filter: 'tags=Walkthrough' };
    const iterator = app.thng().iterator({ params });

    // Read the Thngs
    const thngs = [];
    await EVT.Utils.forEachAsync(iterator, (page) => {
      page.forEach(thng => thngs.push(thng));
    })

    // Map updates to promises and execute
    const promises = thngs.map(thng => thng.property().update([
      { key: 'scan_count',  value: 0 }, 
      { key: 'scan_cities', value: [] }
    ]));
    await Promise.all(promises);
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }
  
  done();
};

Conclusion

The example simple Reactor script is now complete. When a Thng in the project is scanned, it will accumulate a count of scans and the distinct cities it was scanned in as properties. At the beginning of each day, both these properties will be reset.

As an additional exercise you could extend the script to persist the daily totals in the Thng structure (beyond property histories). In the next section a more complex Reactor script will be built, including multiple files and a strong separation pattern based on action types.