Trigger Scripts

Trigger scripts are attached to a database table or query. They run inside of the LabKey Server process using the Rhino JavaScript engine.

  • Note that the current language version supported in our Rhino JavaScript environment for trigger scripts is ECMAScript 5.
Trigger scripts run when there is an insert/update/delete event on a table. They are executed in response to a LABKEY.Query.insertRows() or other HTTP API invocation, or in other contexts like ETLs. Typical uses for trigger scripts include:
  • Validating data and rejecting it if appropriate
  • Altering incoming data, such as calculating a value and storing it in a separate column
  • Updating related tables by inserting, updating, or deleting from them
  • Sending emails or calling other APIs
Trigger scripts are different from transform scripts, which are attached to an assay design and are intended for transformation/validation of incoming assay data.

Topics

Trigger Script Location

Trigger scripts must be part of a deployed module, and are associated with the schema and table based on a naming convention. The script must be under a queries directory, in a subdirectory whose name matches the schema. The file must be named after it's associated table, and have a .js file extension. For example, a QUERY_NAME.js script would be placed in:

Lists:
MODULE_NAME/resources/queries/lists/QUERY_NAME.js
Study Datasets:
MODULE_NAME/resources/queries/study/QUERY_NAME.js
Sample Types:
MODULE_NAME/resources/queries/samples/QUERY_NAME.js
Data Classes:
MODULE_NAME/resources/queries/exp.data/QUERY_NAME.js
Custom Schemas:
MODULE_NAME/resources/queries/SCHEMA_NAME/QUERY_NAME.js

where MODULE_NAME, SCHEMA_NAME and QUERY_NAME are the names of the module, schema and query associated with the table. If you are building a Java module or deploying your module from source, place the scripts within the /resources/ subdirectory of the module tree.

Trigger Script Availability

Trigger scripts are applied in some but not all contexts, as summarized in the grid below. Import pathways are shown as columns here, and datatypes as rows:

 Insert New Row
(single record)
Import Bulk Data
(TSV or file import)
Import via Client APIsImport via Archive
(study, folder, list, XAR)
ETLsscripts
Listsyesyesyesyesyes
Datasetsyesyesyesyesyes
Module/External Schemasyesyesyesnoyes
Assay (see Transform Scripts)nonononono
Sample Typeyesyesyesnoyes
DataClassyesyesyesnoyes

Script Execution

The script will be initialized once per batch of rows. Any JavaScript state you collect will be discarded, and will not be available to future invocations.

If your script runs for more than 60 seconds, it will be terminated with an error indicating that it timed out.

Functions

  • init(event, errors) - invoked once, before any of the rows are processed. Scripts may perform whatever setup the need, such as pre-loading a cache.
  • complete(event, errors) - invoked once, after all of the rows are processed
Depending on what operation is being performed, the following functions, if present in the script, will be called once per row. The function can transform and/or validate data at the row or field level before or after insert/update/delete. The script is not responsible for performing the insert, delete, or update of the row - that will be performed by the system between the calls to the before and after functions.
  • beforeInsert(row, errors)
  • beforeUpdate(row, oldRow, errors)
  • beforeDelete(row, errors)
  • afterInsert(row, errors)
  • afterUpdate(row, oldRow, errors)
  • afterDelete(row, errors)

Parameters and Return Values

  • event - Either "insert", "update" or "delete", depending on the operation being performed.
  • row - An object representing the row that is being inserted, updated or deleted. Fields of this object will represent the values of the columns, and may be modified by the script.
  • errors - If any error messages are added to the error object, the operation will be canceled.
    • errors.FIELD - For row-specific functions, associate errors with specific fields by using the fields' name as the property. The value may be either a simple string, or an array of strings if there are multiple problems with the field.
    • errors[null] - For row-specific functions, use null as the key if the error is associated with the row in general, and not scoped to a specific field. The value may be either a simple string, or an array of strings if there are multiple problems with the row.
  • return false - Returning false from any of these functions will cancel the insert/update/delete with a generic error message for the row.

Example: Validation

This example shows how to apply the validation "'Value' Field Must be a Positive Number"

var console = require("console");

function init(event, errors) {
console.log('Initializing trigger script');
}

function beforeInsert(row, errors) {
console.log('beforeInsert invoked for row: ' + row);
if (!row.value || row.value <= 0) {
errors.value = 'Value must be positive, but was: ' + row.value;
}
row.doubledValue = row.value * 2;
}

Example: Batch Tags for Sample Type Upload

When multiple samples are uploaded at one time, this trigger script tags each sample in the batch with the same 6 digit batch id, to function as a sample grouping mechanism.

// Assumes the sample type has a text field named 'UploadBatchId'.
var console = require("console");
var uploadBatchTag = '';

function init(event, errors) {
console.log('Initializing trigger script');
// Set a 6 digit random tag for this batch/bulk upload of samples.
uploadBatchTag = Math.floor(100000 + Math.random() * 900000);
}

function beforeInsert(row, errors) {
console.log('beforeInsert invoked for row: ' + row);
row.uploadBatchId = uploadBatchTag;
}

Use extraContext

extraContext is a global variable that is automatically injected into all trigger scripts, enabling you to pass data between triggers in the same file and between rows in the same batch. It is a simple JavaScript object that is global to the batch of data being inserted, updated or deleted. The triggers can add, delete or update properties in extraContext with data that will then be available to other triggers and rows within the same batch. If a trigger is fired due to an ETL, the property "dataSource": "etl" will be in the extraContext object. This can be helpful to isolate trigger behavior specifically for ETL or other forms of data entry.

Using extraContext can accomplish two things:

  1. The server can pass metadata to the trigger script, like {dataSource: "etl"}. This functionality is primarily used to determine if the dataSource is an ETL or if there are more rows in the batch.
  2. Trigger scripts can store and read data across triggers within the same file and batch of rows. In the following example, the afterInsert row trigger counts the number of rows entered and the complete batch trigger prints that value when the batch is complete. Or for a more practical example, such a trigger could add up blood draws across rows to calculate a cumulative blood draw.

Example: Pass Data with extraContext

Example trigger that counts rows of an ETL insert batch using extraContext to pass data between functions.

var console = require("console");

function afterInsert(row, errors) {

if (extraContext.dataSource === "etl") {
console.log("this is an ETL");

if (extraContext.count === undefined) {
extraContext.count = 1
}
else {
extraContext.count++;
}
}
}

function complete(event, errors) {
if (extraContext.count !== undefined)
console.log("Total ETL rows inserted in this batch: " + extraContext.count);
}

Console Logging API

A console API is provided for debugging purposes. Import it using the require function. It exports a function as defined by the standard JavaScript console.

var console = require("console");
console.log("** evaluating a trigger script");

The output is available in the labkey.log file, and via JavaScript Console: (Admin) > Developer Links > Server JavaScript Console.

LabKey JavaScript API Usage

Your script can invoke APIs provided by the following LabKey JavaScript libraries, including:

Import these libraries by using require.

Example: Sending Email

To send an email after all of the rows have been processed:

var LABKEY = require("labkey");

function complete(event, errors) {
// Note that the server will only send emails to addresses associated with an active user account
var userEmail = "messagetest@validation.test";

var msg = LABKEY.Message.createMsgContent(LABKEY.Message.msgType.plain, 'Rows were ' + event);
var recipient = LABKEY.Message.createRecipient(LABKEY.Message.recipientType.to, userEmail);
var response = LABKEY.Message.sendMessage({
msgFrom:userEmail,
msgRecipients:[recipient],
msgContent:[msg]
});
}

Note: Unlike how these JavaScript APIs work from the client side, such as in a web browser, when running server-side in a trigger script, the functions are synchronous. These methods return immediately and the success/failure callbacks aren't strictly required. The returned object will the value of the first argument to either the success or the failure callback (depending on the status of the request). To determine if the method call was successful, check the returned object for an 'exception' property.

The require() Function

The parameter to require() is a CommonJS module identifier (not to be confused with a LabKey module) without the ".js" extension. The path is absolute unless it starts with a "./" or "../" in which case it is relative. Relative CommonJS module identifiers can't be used by trigger scripts, but they can be used by other shared server-side scripts in the "scripts" directory.

Order of Execution

When multiple modules are enabled in the container and all include trigger scripts for the table, they will be executed in reverse module dependency order. For example, assume module A has a dependency on module B and both modules have trigger scripts defined for mySchema.myTable. When a row is inserted into myTable, module A's trigger script will fire first, and then module B's trigger script will fire.

Shared Scripts / Libraries

Trigger scripts can import functionality from other shared libraries.

Shared libraries should be located in a LabKey module as follows, where MODULE_NAME is the name of the module and SCRIPT_FILE is the name of the js file. The second occurrence of MODULE_NAME is recommended, though not strictly required, to avoid namespace collisions. The directory structure depends on whether or not you are building from source.

When building the module from source:
MODULE_NAME/resources/scripts/MODULE_NAME/SCRIPT_FILE.js

In a deployed module:
MODULE_NAME/scripts/MODULE_NAME/SCRIPT_FILE.js

For additional information about module directory structure, see Map of Module Files.

In the example below, the 'hiddenVar' and 'hiddenFunc' are private to the shared script, but 'sampleFunc' and 'sampleVar' are exported symbols that can be used by other scripts.

shared.js (located at: myModule/resources/scripts/myModule/shared.js)

var sampleVar = "value";
function sampleFunc(arg) {
return arg;
}

var hiddenVar = "hidden";
function hiddenFunc(arg) {
throw new Error("Function shouldn't be exposed");
}

exports.sampleFunc = sampleFunc;
exports.sampleVar = sampleVar;

To use a shared library from a trigger script, refer to the shared script with the "require()" function. In the example below, 'require("myModule/shared")' pulls in the shared.js script defined above.

myQuery.js (located at: myModule/resources/queries/someSchema/myQuery.js)

var shared = require("myModule/shared");

function init() {
shared.sampleFunc("hello");
}

Invoking Java Code

JavaScript-based trigger script code can also invoke Java methods. It is easy to use a static factory or getter method to create an instance of a Java class, and then methods can be invoked on it as if it were a JavaScript object. The JavaScript engine will attempt to coerce types for arguments as appropriate.

This can be useful when there is existing Java code available, when the APIs needed aren't exposed via the JavaScript API, or for situations that require higher-performance.

var myJavaObject = org.labkey.mypackage.MyJavaClass.create();
var result = myJavaObject.doSomething('Argument 1', 2);

Example Module #1: Attach a trigger script to a query

The example module, testtriggers.module, shows how to attach a trigger script to a table/query. The actual trigger script merely returns the argument it is passed, this example illustrates the mechanism for adding and running scripts.

To run the example:

  • Download the .module file: testtriggers.module.
  • To deploy the module to a production server, follow the topic Module Loading Using the Server UI or copy the .module file to the directory server/externalModules/. For developers who are building the server from source code, place this .module file inside build/deploy/modules/ and restart the server.
  • Turn on the JavaScript Console: (Admin) > Developer Links > Server JavaScript Console.
  • Enable the module in a folder.
  • Navigate to the the module-enabled folder.
  • Go to the Items table: (Admin) > Developer Links > Schema Browser > testtrigger > Items > View Data.
  • Insert a new record using (Insert Data) > Insert New Row.
  • On the server's internal JavaScript console ( (Admin) > Developer Links > Server JavaScript Console), monitor which trigger scripts are run.
  • Repeat by editing or deleting records.

Example Module #2: Several Simple Tests

Other example scripts are available in the module "simpletest", which can be downloaded here: simpletest.zip.

If you want to install this module on a running server, ensure that you do not already have a module of this name. If you are building a local development machine, you may be including the test modules from the LabKey enlistment which contains a simpletest module with similar functions.

Unzip the module structure and review how the scripts and queries are structured to use as a model for your own development. The following example scripts are included:

  • simpletest/scripts/simpletest/Debug.js - a script that will echo a trace log of arguments
  • simpletest/scripts/simpletest/ScriptValidationExports.js - a shared script that returns the arguments (or returns a message if called on a hidden function)
  • simpletest/scripts/validationTest/... - contains a set of trigger scripts that utilize the shared scripts and the LabKey libraries:
    • actionUrlTest.js
    • exportTest.js
    • filterTest.js
    • messageTest.js
    • queryTest.js
    • securityTest.js
    • simpleQueryTest.js
    • utilsTest.js
  • simpletest/queries/vehicle/colors.js - a largely stand alone trigger script. When you insert a row into the Vehicle > Colors query, it performs several format checks and shows how error messages can be returned.
  • simpletest/queries/lists/People.js - a largely stand alone list example. When you insert/update/delete rows in the People list, this trigger script checks for case insensitivity of the row properties map.

Related Topics

Was this content helpful?

Log in or register an account to provide feedback


previousnext
 
expand allcollapse all