Skip to content

JayRule (Jealous Jerboa)

This version of the JayRule include is designed for use with Formbird v3.3.x and later.

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.

This version of the documentation relates to versions 20200529 (Indolent Iguana) and later; for earlier versions see the previous JayRule documentation..

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 (and whether okay or in 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:" or a developer specified prefix.

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.isClientEvent whether the ruleset is running as a client event or a serverside event
ntf.userId the documentId of the current user's account document

Basic Syntax

{

#include "JayRule Ruleset Overlay JS",

  ruleset : {

    name : 'name-of-ruleset',

    rule-name : {

      ruleCondition : rule-condition-function | boolean,

      or

      fieldChanged : string,

      or

      fieldChanged : [strings],

      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 : 'Acme 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

ruleCondition : function(ntf) {...}

Note: this function must not contain asynchronous processes.

Eg

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

ruleCondition can also be assigned simply true (or false) for rules that must always (never) run.

Eg

ruleCondition : true,

fieldChanged

In the case of OnFieldChange rulesets only, we can specify just the name of the field changed. This replaces ruleCondition.

Eg

fieldChanged : 'customerName',

This has the same effect as:

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

We can also nominate multiple fields (Indolent Iguana & later)

fieldChanged : [
    'customerName', 
    'status'
],

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.userId, 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 : {

            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.userId, function(err,result) {
                    if (err) {
                        ntf.logger.error('Error on query: ' + err.message);
                        ntf.errorMessage = 'Error on query: ' + err.message;
                    }
                    else if (result && 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',

    timeout[Seconds|Minutes]: 'timeout in milliseconds / seconds / minutes',

    logging: {

      showUncalledRules : true| false,

      debugEmail : 'email-address' || 'login-email>redirect-email' || [array-of-recipients],

      debugEmailAll : 'true|false',

      logPrefix: 'logging-prefix',

    },

    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.

logging.debugEmail : 'email-address'

When on a server side ruleset, an email containing the logger lines will be sent to the target email address(es) 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.

One login/recipient can be specified as a single string, or multiple in an array.

To login with one email id and have debug emails sent to another email address, use the "target>recipient" notation (see example below).

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

logging : {
    debugEmail : 'adrian.sharples@formbird.com',
    ...
}
// debug emails will be sent to adrian.sharples@formbird.com, when a ruleset runs under that user login id

or

logging : {
    debugEmail : [
        'adrian.sharples@formbird.com>dave.galt@devco.com.au',
        'bill.bailey@fieldtec.com'
    ]
    ...
}
// debug emails will be sent to dave.galt@devco.com.au, when a ruleset runs under the user login id 'adrian.sharples@formbird.com'
// debug emails will be sent to bill.bailey@fieldtec.com, when a ruleset runs under that same user login id.

JayRule, as of v20190819 (Guarded Giraffe) and later, will only send debug emails when the ruleset is triggered by the matching user, ie the login email address must match the debugEmail setting. This was done to reduce the risk of unexpected floods of emails when done by other parties.

This can be overridden with logging.debugEmailAll (see below).

NB - the direct setting (not in logging) will still work with newer versions of JayRule.

logging.debugEmailAll : true|false

When debugEmail has been configured, restores the old feature that emails will be sent on any user triggering the ruleset, not just the specified user as per the debugEmail setting.

JayRule, as of v20190819 (Guarded Giraffe) and later, will only send debug emails when the ruleset is triggered by the matching user, ie the login email address must match the debugEmail setting. This was done to reduce the risk of unexpected floods of emails when done by other parties.

logging : {
    debugEmail : 'adrian.sharples@formbird.com',
    debugEmailAll : true
}

timeout: 'timeout',

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. Without a timeout set, these processes typically just stop, and never return

If not set, timeout defaults to 120000 (120 seconds).
To disable timeout, set to -1.

timeout : 60000  // Set to 60000 milliseconds, or 60 seconds

Please note - having a definite short or medium timeout can be problematic when the ruleset needs to process input from the user, eg a dialog is presented with either OK or Cancel buttons, and separate process is required on each option.

Timeout can also be set in either seconds or minutes, eg

timeoutSeconds : 30,    // 30 seconds
timeoutMinutes : 5,     // 5 minutes
A note on PreSaveServer

Practically, available timeout in PreSaveServer rules is constrained by the limit of the http request timeout, which is around two minutes. Setting the timeout in the ruleset to higher will cause the browser to present a 408 error, even though the ruleset may be carrying on in the background.

It is a good idea to confine PreSaveServer rulesets to the standard 120s (or even 60s); if something more involved is required, it may have to be performed under PostSave, or via a System Action.

logging.logPrefix : 'logging_prefix'

Changes the general logging prefix to a custom one, in case you need to filter logging specifically to this ruleset. Default prefix is "RSLog:"

logPrefix : 'RSLog[CWX]:',

logging : {
    logPrefix : 'RSLog[CWX]:',
    ...
}

logging.showUncalledRules : true|false

Sets whether to show a logging for rules that do not get called due to their condition not being met. Prior to this version, logging of uncalled rules was always shown.

Defaults to false (from version Guarded Giraffe).

JayRule - Using shared functions/objects

The ntf.scope, or ft3, now carries all the "shared" functions and objects available to 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:~~

~~In earlier versions of JayRule, it was necessary to assign ft3 = ntf.scope at the beginning of each function that needs to use it, either the ruleCondition function or the ruleAction function.~~

~~In recent and current versions, ft3 has been made a global object, so this is no longer necessary. You may however see rulesets which do this for each function block -- there is no harm done here, it still works perfectly~~.

UPDATE (Nov 2020):

Unfortunately, ft3 = ntf.scope should be declared for any code-block that requires to use ft3. Experience has shown that in rare instances, multiple rulesets may run at the same time, each one assigning to the shared global object ft3, thus one ruleset may reinitialise ft3 at the expense of added functionality from another ruleset, which then errors.

For the moment we are keeping the global ft3 in place, to maintain compatibility with those rulesets already existing which rely on it. The occurrence of these conflicts seems to be quite rare, so they should work fine apart from the very rare instance. But over time these should all be replaced.

Issues with ft3 on some environments (Nov 2020)

Occasionally, ft3 will lose its assigned functions and properties, particularly in OnLoad rulesets which have modified fields then have asynchronous processes running. This has most often been seen on Chrome browser.

In these cases, an OnFieldChange event is called, running that ruleset while the OnLoad is in progress; when event flow returns to OnLoad, the global ft3 has been modified by the OnFieldChange.

A workaround to this issue is to revert to assigning ft3 explicitly at beginning of ruleActions.

var ft3 = ntf.scope;

Formbird Functions for Rulesets

The explicitly coded functions provided by the FormBird application to the rulesets are available through ntf.scope, or 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 PreSave, 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.

Failing a PreSave Ruleset without dialog

(Available with JayRule (Haughty Hedgehog) & later; Formbird v3.1.5.xxx and later)

Traditionally, when a PreSave ruleset is failed (as per prior section), a dialog is presented to the user with the ntf.errorMessage text.

Often this is not desired, because the developer has already scripted a more suitable dialog, or has prompted for verification with their own dialog, after which they don't want a further dialog to click OK to.

There are two means to effect a failure of the PreSave ruleset without showing the extra dialog:

ntf.rulesetFailed = true;

or

ntf.errorMessage = '(no-display)';

ft3.ModalService.openModal({
  title : 'Widget Not Ready',
  text : 'This widget should have further information. Do you really want to save?',
  showCancelButton : true,
  type : 'warning'
})
.then (
  function confirmF() {
    // do nothing, continue to ruleset completion
  }),
  function cancelF() {
    ntf.rulesetFailed = true;   // Cancel save, without any "oops" dialog
  }

JayRule - Ending a Ruleset (without error)

In order to end a ruleset, simply set ntf.rulesetEnd to true:

ruleNoCascade : {
    ruleCondition : function(ntf) {
        return (ntf.document.flagNoCascade);
    },

    ruleAction : function(ntf) { 
        delete ntf.document.flagNoCascade;
        ntf.rulesetEnd = true;
    }
}

The ruleset will return a success flag to the Formbird system, indicating that the ruleset ended successfully. (ala old method callbackSuccess()). No further rules will be executed.

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.

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) {
                    ntf.errorMessage = 'Error on query: ' + err.message;
                }
                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();    // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            });
        }
    } 

Dialogs

Versions of Formbird later than 3.1.x (currently 3.1.12) no longer support much of the functionality of SweetAlert dialogs that was used in earlier Formbird versions.

It is now necessary to initialise the SweetAlert dialogs for use in rulesets, if complex (eg user interactive) features are used.

To do this,

  • Ensure that your system has the Ruleset Include "SweetAlert Dialog".
  • Include this ruleset include into the top of your ruleset, eg
#include "SweetAlert Dialog",

JQuery & Moment

The "Angular 2" versions of Formbird (v3.3.x onward) no longer natively provide JQuery or the Moment library.

These can be instated for a ruleset via the following #include lines:

#include "JQuery",
#include "Moment.js",

NB

Moment.js is coming to end of life soon. A future version of ECMAScript (Javascript) is likely to have its own date library features, so Moment.js should still be used for the short term future.