Skip to content

JayRule

jaybird

What is a JayRule Ruleset

A JayRule ruleset is one which defines the ruleset's behaviours in a JSON object called "ruleset". This structure is processed by a script from an included RulesetInclude called "JayRule Ruleset Overlay JS", to perform as if it were a fully scripted ruleset.

Prior to JayRule, rulesets are constructed as a JSON/javascript object, but the structure is much looser, more prone to error, and requiring a lot of common "infrastructural" scripting, which JayRule now takes care of behind the scenes.

Features of a JayRule Ruleset

  • Formats each rule in a distinct structured block .
  • Handles synchronous and asynchronous processes behind the scenes.
  • Handles logging for:
  • Start of ruleset
  • Completion of ruleset
  • Start of rule
  • Completion of rule (& whether okay or error)
  • Handles the callback topology needed to maintain a correct ruleset
  • Asynchronous rules only require a single "callback()"
  • hasRun flags automatically set internally.
  • Better error handling and logging.
  • All logging (ntf.logger.info, etc) prefixed with "RSLog:"

Added Features

The following are ready made state properties of the ntf object

Property Description
ntf.isNew whether document is new or existing
(nb -- this is incorrect for PostSave, use following in initialisation:
ntf.isNew = (sh && sh.previousVersionId) ? false : true;
ntf.isClientContext whether the ruleset is running as a client event or a serverside event
ntf.userId the documentId of the current user's account document

Syntax

{

#include "JayRule Ruleset Overlay JS",

  ruleset : {

    name : 'name-of-ruleset',

    rule-name : {

      ruleCondition : rule-condition-function,

      ruleAction : rule-action-function

    },

    rule-name : {

      ...

  }

}

NB: This is a simplified syntax -- other optional features of the syntax are explained in later documents.

ruleset

The main object. Must be named "ruleset".

name : name-of-ruleset

The name of the ruleset to use in the logging of the ruleset, eg

name : 'CWW Case - OnLoad',

rule-name

Identifier/key of the rule. Must be distinctly named. Must begin with lowercase "rule". Eg

ruleInitialiseFacts : {

Each rule will be run in the sequence as ordered within the ruleset.

rule-condition-function

A function that returns true (or non-empty/non-zero) if the rule is to proceed.

This function must be of the format

function(ntf) {...}

Note: this function must not contain asynchronous processes.

Eg

    ruleCondition : function(ntf) {
        return (ntf.context.fieldChanged === 'checkVenomous');
    },

rule-action-function

A function that performs the rule's action.

If asynchronous, the function must be of the format:

function(ntf, callback) {...}

By adding the callback argument, we are telling the ruleset that we want it to wait for an asynchronous process within this rule to complete, before going onto the next rule.

Eg

  ruleAction : function(ntf,callback) {
    var ft3 = ntf.scope;
    var eqry = {"query": {"bool": {"filter": [
      {"term" : {"documentId" : ntf.user.documentId}}
    ]}}};

    ft3.findDocumentsByElastic(eqry, ntf.user.documentId, function(err,result) {
      if (result) {
        var udoc = result.data.hits.hits[0]._source;
        ntf.document.description = 'Email fetched: ' + udoc.email;
      }
      callback();
    });
  }

If not asynchronous, it should be of the format:

function(ntf) {...}

With this format, the ruleset will simply run the contained code through, and then proceed to the next rule.

Eg

    ruleAction : function(ntf) {
        var ft3 = ntf.scope;
        var flag = (ntf.document.checkVenomous === true);
        ft3.showField('shWarning', flag);
    }

Example

The following is a simple ruleset including two non-asynchronous rules, plus one asynchronous rule.

// RuleSet: Dexwise Spider - OnFieldChange
// Updated by: peter.dexter@fieldtec.com 2018-03-03 17:23:49 +11:00
{
#include "JayRule Ruleset Overlay JS",

    ruleset : {

        name : "Dexwise Spider - OnFieldChange",

        // ---------------------------------------------------------------------
        // ruleVenomous
        // Shows/hides a warning based on the checkbox "Venomous"
        // ---------------------------------------------------------------------
        ruleVenomous : {

            ruleCondition : function(ntf) {
                return (ntf.context.fieldChanged === 'checkVenomous');
            },

            ruleAction : function(ntf) {
                var ft3 = ntf.scope;
                var flag = (ntf.document.checkVenomous === true);
                ft3.showField('shWarning', flag);
            }
        },

        // ---------------------------------------------------------------------
        // rulePopGenusSpecies
        // If entered description is 'abc123', 
        //   set genus to 'Arceteuthis' and species to 'Duxiformi'
        // ---------------------------------------------------------------------
        rulePopGenusSpecies : {

            ruleCondition : function(ntf) {
                return (
                    ntf.context.fieldChanged === 'description'
                    && ntf.document.description === 'abc123'
                );
            },

            ruleAction : function(ntf) {
                delete ntf.document.description;
                ntf.document.genus = 'Arceteuthis';
                ntf.document.species = 'Duxiformi';
            }
        },

        // ---------------------------------------------------------------------
        // ruleGetUserByQuery
        // Queries for the user account document and outputs the email to the 
        // description.
        // NB - we could just use the object ntf.user, but we want to demonstrate asynchronous query usage here
        // ---------------------------------------------------------------------
        ruleGetUserByQuery : {

            ruleCondition : function(ntf) {
                return (
                    ntf.context.fieldChanged === 'commonName'
                    && ntf.context.newValue === 'ubug666'
                );
            },

            ruleAction : function(ntf,callback) {
                var ft3 = ntf.scope;
                var eqry = {"query": {"bool": {"filter": [
                    {"term" : {"documentId" : ntf.user.documentId}}    
                ]}}};

                ft3.findDocumentsByElastic(eqry, ntf.user.documentId, function(err,result) {
                    if (err) {
                        var msg = err.message || JSON.stringify(err);
                        ntf.logger.error('Error on query: ' + msg);
                        ntf.errorMessage = 'Error on query: ' + msg;
                    }
                    else if (result && result.data && result.data.hits && result.data.hits.total) {
                        var udoc = result.data.hits.hits[0]._source;

                        ntf.document.description = 'Email fetched: ' + udoc.email;

                        delete ntf.document.commonName ;
                    }
                    callback();
                });
            }
        } 
    }
}

JayRule Extended Syntax

This document outlines the further main properties that may be set on the JayRule "ruleset" object

Syntax

{

#include "JayRule Ruleset Overlay JS",

  ruleset : {

    name : 'name-of-ruleset',

    debugEmail : 'email-address',

    timeout: 'timeout-in-milliseconds',

    rule-name : { ...   }

}

ruleset

The main object. Must be named "ruleset".

name : name-of-ruleset

The name of the ruleset to use in the logging of the ruleset, eg

name : 'CWW Case - OnLoad',

NB -- If you set this to '{{{rulesetName}}}' (as is default for a new ruleset), then on saving, the name/object event descriptor of the ruleset will be substituted in.

debugEmail : 'email-address'

(Available from v20180828 (Cautious Capybara))

When on a server side ruleset, an email containing the logger lines will be sent to the target email address on completion of each run of the ruleset. Any lines issued to the log via ntf.logger.info or .debug or .error will be sent. Use for debugging purposes only, of course.

Requires that the server is configured to send emails, and is working. Depending on any syntax errors, may fail to send.

debugEmail : 'adrian.sharples@formbird.com',

timeout: 'timeout-in-milliseconds',

(Available from v20180828 (Cautious Capybara))

Sets the maximum time that should be allowed for the whole ruleset to run, in milliseconds.

This can be useful for tracking processes that fail to complete, or where a rule requires a callback, but it has not been made.

If not set, timeout defaults to 20000 (20 seconds).
To disable timeout, set to 0.

timeout : 60000  // Set to 60 seconds

JayRule - Using shared functions/objects

The ntf.scope, or ft3, now carries all the functions and objects previously shared in rulesets, including

  • the "global" functions, eg showField(...)
  • functions and objects scripted before/after the ruleset definition
  • the objects from #includes

These are available in both the ruleCondition and the ruleAction.

NOTE: You must assign ft3 = ntf.scope at the beginning of each function that needs to use it, either the ruleCondition function or the ruleAction function.

Formbird "global" Functions for Rulesets

The explicitly coded functions provided by the FormBird application to the rulesets are available through ntf.scope, or an assigned ft3, as previously practised.

Eg

{
#include "JayRule Ruleset Overlay JS",

    ruleset : {
        ruleDoThingA : {
            ruleCondition : ...,

            ruleAction : function(ntf) {
                var ft3 = ntf.scope;
                ft3.showField('dateInitiated', true);
                ...
            }

Functions/objects from #include

Functions and objects defined within a #include rulesetInclude are available in ft3 (ntf.scope).

Eg

{
#include "JayRule Ruleset Overlay JS",
#include "Basic Functions JS", // provides object "basicFunctions"
...

ruleset : {
    ruleDoThingA : {
        ruleCondition : function(ntf) {
            var ft3 = ntf.scope;
            // Perform function from #include "Basic Functions
            var x = ft3.basicFunctions.datePrecedes(ntf.document.date1, ntf.document.date2);
            ...
        }

Functions/objects from the ruleset

Functions and objects defined within the main block of the script (sibling to the object "ruleset") are available in ft3 (ntf.scope).

Eg

{
#include "JayRule Ruleset Overlay JS",

    getFullName : function(field) {
        var name = field.firstName + ' ' + field.surname;
        return name;
    },

    ruleset : {
        ruleDoThingA : {
            ruleCondition : function(ntf) {
                var ft3 = ntf.scope;
                // Test full name
                var x = ft3.getFullName(ntf.document.contact);
                return (x === 'Peter Dexter');
            }
            ...
        }
        ... 
   }
}

JayRule - Raising Errors

If a ruleAction process results in the ruleset needing to abort, with an error statement, eg on validating fields in OnFieldChange, the process is:

  • set ntf.errorMessage to the error you want displayed/reported

To terminate the block at that point:

  • callback(), if the ruleAction has a callback in its function signature, ie is asynchronous; if not, ignore.
  • return; (for either asynchronous/non-asynchronous)

Example

if (ntf.userType === 'forbidden') {
  ntf.errorMessage = 'Cannot perform this action for forbidden User Type';
  return;
}

That's it.

If there is no return/callback, the ruleset will terminate at the completion of that rule block.

Converting from regular JSON Ruleset to JayRule format

Switching from a regular JSON ruleset is fairly straightforward.

For non-asynchronous rules

JSON Normal layout of a rule block (non-asynchronous)


// ----------------------------------------------------
// Rule "ABC"
// ----------------------------------------------------
if (
    !ntf.hasRun.ruleABC
    && condition
) {
    ntf.logger.info('In rule "ABC");
    action
    ntf.hasRun.ruleABC = true;
}


to JayRule ruleblock (non-asynchronous):


// ----------------------------------------------------
// Rule "ABC"
// ----------------------------------------------------
ruleABC : {
    ruleCondition : function(ntf) {
        return (condition);
    },

    ruleAction : function(ntf) {
        action
    }
}


Note, the ntf.hasRun.flag does not need to be set, nor does the logger line for the rule starting.

For asynchronous rules


// ----------------------------------------------------
// Rule "ABC"
// ----------------------------------------------------
if (
    !ntf.hasRun.ruleABC
    && condition
) {
    ntf.logger.info('In rule "ABC");
    action(..., function(..) {
        ...
        ntf.hasRun.ruleABC = true;
        callbackContinue(ntf);
    });
    return;
}


to JayRule ruleblock (asynchronous):


// ----------------------------------------------------
// Rule "ABC"
// ----------------------------------------------------
ruleABC : {
    ruleCondition : function(ntf) {
        return (condition);
    },

    ruleAction : function(ntf, callback) {
        action(..., function(..) {
            ...
            callback();
        });
    } }


Note, the ntf.hasRun.flag does not need to be set, and return does not need to be added, nor does the logger line for the rule starting.

JayRule - Running Asynchronous Rules

Rules that require to run an asynchronous process should be formatted as below:


ruleName : {
    ruleCondition : function(ntf) {
        ...
    },

    ruleAction : function(ntf, callback) {
        ...
        asyncFunction(..., function(args) {
            ...
            callback();
        }
    }
}


Note the salient features:

  • ruleAction has two arguments -- ntf and callback
  • callback() is called at the endpoint of the rule, typically within the callback of an asynchronous function

CAUTION

It is VITAL that your rule only calls callback() once in the course of a rule. It may be written several times within the rule, but only one should ever be called in any one running of the rule.

Failing to ensure this may result in failure of the ruleset to run properly.

Current versions of the JayRule overlay will only process for the first callback, and prevent 2nd , 3rd, 4th, etc callbacks from proceeding. The logging will display "RSLog:!!! Multiple callbacks detected - aborting"

Example

    // ---------------------------------------------------------------------
    // ruleGetUserByQuery
    // Queries for the user account document and outputs the email to the 
    // description.
    // NB - we could just use the object ntf.user, but we want to 
    // demonstrate asynchronous query usage here
    // ---------------------------------------------------------------------
    ruleGetUserByQuery : {

        ruleCondition : function(ntf) {
            return (
                ntf.context.fieldChanged === 'commonName'
                && ntf.context.newValue === 'ubug666'
            );
        },

        ruleAction : function(ntf,callback) {        // <<<<<<<<<<<<<<<<<<<<<<<<<<<<
            var ft3 = ntf.scope;
            var eqry = {"query": {"bool": {"filter": [
                {"term" : {"documentId" : ntf.user.documentId}}    
            ]}}};

            ft3.findDocumentsByElastic(eqry, ntf.userId, function(err,result) {
                if (err) {
                    var msg = err.message || JSON.stringify(err);
                    ntf.logger.error('Error on query: ' + msg);
                    ntf.errorMessage = 'Error on query: ' + msg;
                }
                else if (result && result.data && result.data.hits && result.data.hits.total) {
                    var udoc = result.data.hits.hits[0]._source;

                    ntf.document.description = 'Email fetched: ' + udoc.email;

                    delete ntf.document.commonName ;
                }
                callback();    // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            });
        }
    } 
    ...

    // -----------------------------------------------------------
    // Rule "PrePopSeries"
    // Runs when user types "ubug667" into the commonName field.
    // This prepares the array for processing
    // -----------------------------------------------------------
    rulePrePopSeries : {
        ruleCondition : function(ntf) {
            return (
                ntf.context.fieldChanged === 'commonName'
                && ntf.context.newValue === 'ubug667'
            );
        },

        ruleAction : function(ntf) {
            ntf.doPopSeries = true;
            ntf.popSeriesDesc = '';
            // Prepare the array for later processing
            ntf.arrPopBits = ['alpha','bravo','charlie','delta','echo','foxtrot',
                 'golf','hotel','india','juliet'];
        }
    },

    // -----------------------------------------------------------
    // Rule "PopByArray"
    // This processes each item of the array prepared above.
    // (Attaches a formatted 'tag' value to the current User)
    // -----------------------------------------------------------
    rulePopByArray : {
        ruleCondition : function(ntf) {
            return (ntf.doPopSeries);
        },

        arrayToProcess : function(ntf) {
            return ntf.arrPopBits;
        },

        ruleItemProcess : function(ntf, item, index, callback) {
            var ft3 = ntf.scope;
            ft3.showNotification('Processing ' + item + ' (#' + index + ')...');

            // Attach 'spiderTag-xxxxx' to current User document 
            var tagName = 'spiderTag-' + item;
            var tagValue = item + ft3.moment().format('YYYYMMDD-HHmmss');
            var change = {};
            change[tagName] = tagValue;

            var userId = ntf.user.documentId;

            ft3.updateDocument(userId, change, userId, function(err,doc) {
                if (err) {
                    ntf.errorMessage = 'Error on update: ' + err.message;
                }
                else {
                    ntf.logger.info(tagName + ' attached to User');
                    ft3.showNotification(tagName + ' attached to User');
                }
                callback();
            });

        },  

        ruleAction : function(ntf) {
            // Final tasks after array is processed
            ntf.document.description =  ntf.popSeriesDesc;
            ntf.logger.info('Array Processing complete hey hey');
        }
    },

    ...