Lesson 10. Writing Queries/Asynchronous Rules
Updated pdexter 2022-12-20
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.
(2022-12-20) More recently, we are able to take advantage of Promise based calls to make these asynchronous functions look simpler and more "synchronous". We will employ these means below.
Querying in Formbird Rulesets
Querying in rulesets is, at root, 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.
However, we can use a library function ft3.getDocsByEqry to make coding simpler.
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 : {
fieldChanged : 'buttonPropagateChanges'
},
- Set the ruleAction function to carry an argument "callback"
ruleAction : async 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.
The keyword "async" enables us to use the "await" keyword later.
- 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/7.17/query-dsl.html
- Make the call to ft3.getDocsByEqry
ntf.bookingDocs = await ft3.getDocsByEqry(ntf, eqry);
- Handle a valid return of data
ntf.bookingDocs = await ft3.getDocsByEqry(ntf, eqry);
ntf.logger.info('# Bookings: ' + ntf.bookingDocs.length);
- Finally, call callback() at the end of the function block:
callback();
The full ruleAction function should be:
ruleGetChildBookings : {
ruleCondition : true,
ruleAction : async function(ntf, callback) {
var ft3 = ntf.scope;
var eqry = {"query": {"bool": {"filter": [
{"term" : {"appTags" : "acme"}},
{"term" : {"appTags" : "rentalCarBooking"}},
{"term" : {"rentalCarRel.documentId" : ntf.document.documentId}}
]}}};
ntf.bookingDocs = ft3.getDocsByEqry(ntf, eqry);
ntf.logger.info('# Bookings: ' + ntf.bookingDocs?.length());
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)