Skip to content

Lesson 10. Writing Queries/Asynchronous Rules

What is an Asynchronous Function?

One of the great challenges to learning javascript writers (ergo Ruleset writers), is the concept of dealing with asynchronous processes.

Most of the processes you've encountered in these lessons have been synchronous, that is, each command is called, and completes, before we move onto the next command. Eg, ft3.mandateField happens immediately, and doesn't require any waiting for completion.

Many functions in javascript however are not synchronous , rather they are asynchronous. That is, they start processing, but control continues immediately to the next command. This usually involves a process that takes a little while to do, and returns data, such as a query to a database.

Consider the calling of three simple functions

var a = functionA();  // returns 42
var b = asyncFunctionB(3);  // returns 63 after several seconds
var c = functionC(5); // returns 84
ntf.logger.info('a/b/c:' + a + '/' + b + '/' + c);

The output we would see in the log would be:

'a/b/c:42//84'  // missing b value

This is because asyncFunctionB has only just started running when functionC is called then the logger command made. We have not waited for it to complete.

Instead we need to code

var a = functionA();  // returns 42
asyncFunctionB(3, function(data) {  // returns 63 after several seconds
    var b = data;
    var c = functionC(5); // returns 84
    ntf.logger.info('a/b/c:' + a + '/' + b + '/' + c);
});

Asynchronous functions receive a final argument that is a function to be run when it is completed. So we need to provide that function if we want to deal with any returned result of that asynchronous function.

One common such function we use in rulesets is the function findDocumentsByElastic, which queries our database and returns a result. Because this is used in a lot in rulesets, the issue of asynchronous processes rises quite often.

This makes for quite a complicated layout of coding, in several layers of callbacks. However, the right use of ruleset rules can level out this complexity.

Querying in Formbird Rulesets

Querying in rulesets is done via the function findDocumentsByElastic, which takes ElasticSearch queries. We will step through the scripting of such a query below.

For further details, see findDocumentsByElastic.

Aim of the Lesson

In this lesson, we will create a query to gather the child Booking documents of a Rental Car document.

For ease of debugging, we will do this in an OnFieldChange ruleset.

In a later lesson, we will build on that to copy changes of the Rental Car document out to each Booking document.

Preparation - Add a new button

  • Open the template for Rental Car.
  • After the component entry for createdByUser, add a new button field.
{
    "componentName": "sc-button",
    "name": "buttonPropagateChanges",
    "caption": "Propagate Changes"
},

The caption "Propagate Changes" will make more sense in a later lesson building on this one.

Step 1. Add an asynchronous rule

In this step, we add a rule to our previous OnFieldChange ruleset, to gather the child documents of the Rental Car document.

  • Open your previous Rental Car OnFieldChange ruleset
  • As preparation, add a new ruleset include directive to the top of the ruleset:
#include "ft3 Extension Functions",

This will help us later with dealing with query results.

  • Add a new rule called "ruleGetChildBookings".
  • For the ruleCondition, run when the button Propagate Changes is clicked.
ruleCondition : function(ntf) { 
    return (ntf.context.fieldChanged === 'buttonPropagateChanges');
},
  • Set the ruleAction function to carry an argument "callback"
ruleAction : function(ntf, callback) { 

This tells the ruleAction that it is using asynchronous processes, and that processing should not continue to the next rule until that "callback" function has been called.

  • Add an ElasticSearch query variable to the ruleAction to query for the child Booking documents for this Rental Car document.
var eqry = {"query": {"bool": {"filter": [
    {"term" : {"appTags" : "acme"}},
    {"term" : {"appTags" : "rentalCarBooking"}},
    {"term" : {"rentalCarRel.documentId" : ntf.document.documentId}}
]}}}; 

For detail on ElasticSearch queries, see https://www.elastic.co/guide/en/elasticsearch/reference/2.4/query-dsl.html

  • Make the call to ft3.findDocumentsByElastic
ft3.findDocumentsByElastic(eqry, ntf.userId, function(err, result) {

});
  • We code the response to this function within its callback function block.
  • Define the object esResult
ft3.findDocumentsByElastic(eqry, ntf.userId, function(err, result) {    
    var esResult = ft3.esResult(result);
    ...

ft3.esResult is a handler utility for dealing with the result of an elasticsearch query. We create the local object esResult, and use this to interrogate our query result.

For more information on esResult, see ft3 Extension Functions #esResult

  • Handle any error from the query
ft3.findDocumentsByElastic(eqry, ntf.userId, function(err, result) {
    ...
    if (err) {
        ntf.logger.error('Error on query: ' + err.message);
    }
    ...
});
  • Handle a valid return of data
ft3.findDocumentsByElastic(eqry, ntf.userId, function(err, result) {
    ...
    else if (esResult.isValid()) {
        ntf.logger.info('Query returned ok.');
        ntf.logger.info('# Bookings: ' + esResult.count());                

        // Set booking docs for later rule
        ntf.bookingDocs = esResult.toDocArray();
    }
    ...
});
  • Finally, call callback() at the end of the function block:
ft3.findDocumentsByElastic(eqry, ntf.userId, function(err, result) {
    ...
    ...

    callback();
});

The full ruleAction function should be:

ruleGetChildBookings : {
    ruleCondition : function(ntf) { 
        return (true);
    },

    ruleAction : function(ntf, callback) { 
        var eqry = {"query": {"bool": {"filter": [
            {"term" : {"appTags" : "acme"}},
            {"term" : {"appTags" : "rentalCarBooking"}},
            {"term" : {"rentalCarRel.documentId" : ntf.document.documentId}}
        ]}}}; 

        ft3.findDocumentsByElastic(eqry, ntf.user.documentId, function(err, result) {
            var esResult = ft3.esResult(result);
            if (err) {
                ntf.logger.error('Error on query: ' + err.message);
            }
            else if (esResult.isValid()) {
                ntf.logger.info('Query returned ok.');
                ntf.logger.info('# Bookings: ' + esResult.count());                

                // Set booking docs for later rule
                ntf.bookingDocs = esResult.toDocArray();
            }
            callback();
        });
    }
}
  • Save your ruleset.

Step 2. Testing

  • Open an existing Rental Car document.

  • Open the debugger console by pressing F12.

  • Change the registration number on the Rental Car.

  • Save

  • Observe the generated logging in the debugger console.

  • You should see the rule "ruleGetChildBookings" being run, and a display of the number of child Bookings for this Rental Car document.

Lesson Items covered

  • Asynchronous function handling.

  • Creating an asynchronous rule.

  • Querying from a ruleset.

  • Introduction to ft3 Extension functions (esResult)