ModScript, at least since version 11.3, added a new "Log" object with methods to allow sending text to a log file in the SBM "Application Engine\Log" directory. To view these messages you need to start a "Remote Desktop Connection", aka "Remote Console" or RDP to the AE server and view the log file in the "Application Engine\log" directory. Previously, this kind of logging was done using the "Ext.LogInfoMsg" call which sent the message to the AE server's Windows Application Event log. To view these log messages, you also had to "remote" into the AE server, then use the Windows Event Viewer to view the Application Event log. That level of access can make some clients or server administrators nervous. In some conditions, the developer may not have any access to SBM servers.

Fortunately, there's a way to create a log that doesn't require any remote access to the AE server, outside of IIS.

Some basic "one time" setup is necessary to allow this capability. Have someone with "administrator" privileges on the SBM AE server do the following steps:

"remote" into the SBM AE server

"CD" to IIS's "inetpub\wwwroot" directory.

Create a subdirectory under "inetpub\wwwroot" for the ModScript log files. In this example the name of the subdir is "AE-Logs".

Use the Microsoft Sys Internals "junction" tool "junction" to create a file junction from "inetpub\wwwroot\AE-Logs" that is targeted at a subdirectory (which the "junction" command will create) under the App Engine's "Log" directory. In this example the name of the subdir under "Application Engine\Log" is called "inetpub_AE-logs".

Now your ModScript can use the following calls to create a Log file in that new subdir. Make sure that the name of the log file you create has a file extension that IIS will handle, like ".txt" or ".htm". On my system, IIS will not handle a file with the ".log" extension. I can probably change that by making changes to the IIS settings.

In ModScript, we can make calls to external REST APIs. Being able to pull in data or send data to a REST API really grows the ability to build integrations with ModScript. In my example, I use the experimental SBM feature Data Service. Data Service allows us to create a connection to a database, which can be the current SBM database or any other database via an ODBC DSN, and pre-configure an SQL query that can be requested via REST. The SQL query can have runtime parameters bound to them from URL parameters.

SBM Data Service

The SBM Data Service is an experimental feature. As such, it must be enabled in the TS_SYSTEMSETTINGSNAMESPACED table:

update TS_SYSTEMSETTINGSNAMESPACED set TS_LONGVALUE=1 where TS_NAME='EnableDataServices'

Next, edit DataServiceConf.xml in SBM\Application Engine\bin. Read the big comment in the xml file to get more information about the different types of connections you can use in Data Service. In my example, I create a DSN-based Connection, even though I could just use a local-SBM connection. The reason I do this is because I think it is likely that my reader will really want to pull data from a different database rather than pull data from the SBM database. ModScript has powerful SQL querying features for querying the SBM database, and probably will not need to invoke a REST service to do it. However, ModScript cannot peek into a non-SBM database using its SQL query features; as such, it is a more likely use case to invoke Data Services via REST to query a separate database. However, to keep the example simple, I use the DSN-based Connection to peek back into the SBM database, as I know it is a database that my readers have on-site.

In this example, we search the primary table USR_MODSCRIPTRESTCALL for items where TS_ACTIVEINACTIVE is equal to the value passed on the URL, and TS_SUBMITTER is equal to the value passed on the URL. We return the TS_ID, TS_TITLE, TS_SUBMITTER, and TS_SUBMITDATE. This query could be much more complex. It could create a temp table, insert data into it, then join to that data to collate it all into the desired output. However, this example is not about fancy SQL queries, so we keep it simple.

The DataServiceConf.xml file is only processed on AE startup to keep the service fast. Unfortunately, this means IIS has to be reset any time you change the file. So, after an IIS reset, a call to the DataService URL will set up the query, bind the URL parameters to the query, and execute it.

ModScript

In this example, the ModScript will be fairly simple. It requests to data from Data Service and injects it into the HTML form as a JavaScript variable. I added this script as a pre-transition action on the submit transition of my workflow. What this will do is show the submitter all other items they submitted into this table that are still active. Attached is my example application.

Is my example a little contrived? Sure, the form could have invoked Data Service directly, but we are pretending that the ModScript did something important with the JSON before writing it to the form. Also, in many use cases, there is no form involved. Instead, ModScript will invoke a REST API to write data to some integration, or use ModScript to pull data from an external source into a field on the item. Also, ModScript could have written a more complex JavaScript into the page to do all the stuff we do on the form. However, writing JavaScript from inside ModScript is not very fun, as you have to be sure to encode everything correctly. Quoted text can be really annoying. Instead, use ModScript to write the data onto the form can be pretty simple, and then the JavaScript can take it from there. In a more complex example, I would move the JavaScript to its own file, simply exposing a simple function for the HTML/JavaScript widget to invoke (this makes the JavaScript reusable, easier to write using Composer's syntax highlighter, and easier to get to).

Contrived or not, we can see ModScript in action, directly invoking a REST call and doing something with the data.

What's new in SBM 11.4

ModScript in 11.3.1 has the ability to make REST calls. In 11.4, we greatly increased the flexibility of the REST call functionality. Features added in SBM 11.4 for REST callouts:

Custom URL path parameters

11.3.1 allowed the scripter to add and change URL parameters (values after the ? in the URL). 11.4 extends this to the URL path (values after the protocol, server, and port but before the ?). This allows the scripter to add a single REST Data Source that points to the protocol/server/port to whatever REST API you are interacting with, then add URL path values to invoke the REST functions required.

11.3.1 allowed the scripter to invoke REST calls via POST and GET, but 11.4 extends this to PUT, DELETE, and any custom HTTP REST verb desired.

11.4 allows the scripter to add any custom header to the HTTP call.

11.4 allows the scripter to get the headers from the HTTP result.

11.4 allows the scripter to bypass the SBM Proxy if desired.

SBM Proxy adds functionality such as the ability to support OATH2, but sometimes it might be desired to bypass the SBM Proxy to avoid any complexity added.

Bypassing can help with debugging a REST call that is having a problem (see if the problem is due to SBM Proxy or not).

As a side note, not related to REST but tangential to this example: In 11.3.1, ModScript has functions like Db.ReadIntegersWithSQL(), but the column types are rigid (you cannot read an int, 2 strings, and a double), which might make using Data Service look good for custom queries on the SBM database. 11.4 has Db.ReadDynaSQL(), allowing any number of columns to be returned in a query, making for extremely flexible database querying of the SBM AE schema. As such, you would only need Data Service for querying external databases.

Sometimes, we want more information on a custom form, but we can't figure out how to get it. The answer might be a call to SBM JSON API using a REST widget. However, if you just can't seem to find a non-scripty way to get the data you want, consider invoking ModScript from the form. I have put together a sample application based on 11.3.1 (see 11.4 example below for updated version) which shows how you could do this. In my example, the process app has a Contact field, and I want to show more information about that contact and the company that they are part of. To do this, I wrote a ModScript that can be invoked via the Direct URL context. It requires that the contact ID be passed in, either as a URL parameter or as a JSON value in the body of the HTTP call. You wouldn't really need to provide flexibility like that, but here we are trying to give an example of both so that we can really see how to send data to ModScript in the Direct URL context.

Here, you see that we first look at the Shell's "PostData()" value. This will hold the body of the HTTP request, as long as the request is a POST, the HTTP request "Content-Type" is "application/json", and the data length is less than our maximum allowed in the "ScriptPostDataMax" system setting (which defaults to 10 MB). So, first, the ModScript checks to see if we have any data in Shell.PostData(). If so, it casts the value to a string (most values in the Shell are Variant), then invoke from_json() on the string. Assuming that the JSON passed in was { "contact": 123 }, this should give us a Map with a single entry of "contact". So, the script immediately requests the value "contact" from the Map. Of course, the data sent in to ModScript could be quite complex, in which case you could catch the return value of "from_json()" and then process it.

If we did not get anything in Shell.PostData(), we then look in the Shell.Params(), which is a Dictionary of the URL parameters passed to this Direct URL call. In this case, we expect to find a value "contact" as a URL parameter. Values in Dictionaries are also Variant, so we need to cast it. Unfortunately, until 11.4, we do not have a direct Variant.to_int(), so we use Variant.to_string(), then use string.to_int() to finally get an integer.

This is pretty straight forward. Create a VarRecord object for interacting with the "TS_CONTACTS" table. If we were not passed a contactID that we can read, we return an empty JSON object (the JavaScript consumer of this output expects JSON, so be sure to send valid JSON, even when there is an error).

Here, we see a few important things. First, after calling VarFieldList.FindField() or VarFieldList.FindSysField(), you may have a null object (if the field could not be found). It is important to test for null using "is_var_null()". Next, you see a lambda which takes a Field and a Variant, it invokes Field.GetDbValue() and returns the value. This is because, until 11.4, ModScript did not have a version of Field.GetDbValue() that returned the value directly to the caller. Instead, I need a way to call Field.GetDbValue() after the check for is_var_null() but while still inside the "if" (or I could do nested "if" statements, I chose the lambda). Immediately after the lambda is declared, we invoke it with our Field and Variant, and let it give us the value that we can test.

Finally, we have the Company ID and we can read the Company to get the address. I put all of this together into a ModScript Map, then invoke "to_json()" to get a nice JSON string that JSON-encodes the embedded text values and formats the return value.

The JavaScript Script

On the JavaScript side, I have two examples to show both a POST with a JSON body and a GET with the data on the URL. Both put the results onto the custom form in the

location specified. The JavaScript writes an unordered list of information using the returned JSON. Keep in mind that in SBM, the jQuery object is called "jQuerySBM", not "$".

The Form

To pull it all together, I created a State form for my one and only workflow state. In the form's properties, I selected my custom JavaScript. I also ensured that "Include jQuery plugin" was checked. I added an HTML/Javascript widget, with the following HTML and JavaScript:

This invokes my two examples, and injects a couple locations in the HTML for the JavaScript to write out the results.

What's New in 11.4

I wrote this application to work in 11.3.1. In 11.4, we could have simplified our script. Also, we could bind the JSON output directly to a REST Grid Widget on a form. I have updated the sample application with the changes. A few notable differences:

Ext.SetContentType( "application/json" );

In 11.4, ModScript can set the declared Content-Type for the Direct-URL context. This is important as the REST Grid Widget does not allow you to bind to a request that returns a Content-Type of "text/html".

The JSON returned is now a JSON array so that the REST Grid Widget recognizes it.

Since we are using the REST Grid, we don't really need the HTML/JavaScript widget or the JavaScript file. However, I kept them so we can see all the different options. The JavaScript doesn't need to call JSON.parse(data), the new Content-Type of "application/JSON" indicates to the underlying engine that it should parse the JSON and give us an object. The script was changed to access index 0 of the array, as the JSON is now an array with our object inside.

To use the REST Grid Widget, I added an Endpoint that points to the ModScript call that we had been making via JavaScript. Be sure to edit the endpoint in Application Repository to point to your AE server. The REST Grid Widget can bind the Contact ID to the URL parameter.

11.4 has VarRecord.GetFieldValueString(), but it still requires the string to be passed in rather than returned directly to the caller (this is so that it can return false if the field is not found). I added a function to the Field class in my script that will return the string value directly to the caller, which makes it easier to concatenate the address string. Only do this if you are SURE all the fields will be found:

ModScript has the ability to invoke a function exposed from a DLL. The parameters passed to the DLL from ModScript are input/output, meaning that ModScript can send any data to the DLL, and the DLL can send any data back. However, the DLL function must have a specific function signature, so it is not possible to invoke a DLL function that was not designed to be called from ModScript.

The DLL

I'll use C++ for the DLL. First, declare the type "SBMScriptArg". This will be they type for each function parameter passed from ModScript to the DLL:

struct SBMScriptArg
{
char* pData;
int size;
};

Also, typedef the ReallocArg_t function, which is a callback function that the DLL can use to resize a parameter, ensuring it will be big enough for the output value:

typedef int( *ReallocArg_t )( SBMScriptArg* pArg, int newSize );

Finally, give yourself a set of functions for setting an argument to a string, int, etc:

With this in place, the DLL can reset any parameter sent from ModScript to a new value, be it a string with SetArg(), or an int, double, etc with SetArgT(). Now, define the function that ModScript will invoke. In my example, the function is called "DoIt".

The extern "C" __declspec( dllexport ) ensures that ModScript will be able to find the function in the DLL. The function must return an int, and have the parameter signature ( SBMScriptArg* args, int argCount, ReallocArg_t reallocate ). After that, the function can do whatever you desire. In my use case, the function first loops through the parameters passed in and prints the value to console (may not be useful if you are running ModScript in AE under IIS). Then, if there is at least 1 parameter, it sets the first parameter to "what". If there are at least 2 parameters, it sets the second to 12345. These values are visible in ModScript after the DLL function completes. Finally, an integer value is returned. I have bundled this C++ together in an example Visual Studio project.

Notes:

Be sure to save the source code for the DLL, possibly in a zip that you include on the SBM Application Engine machine, right next to the DLL.

It is usually best to compile in "release" mode, as "debug" mode often requires debug C++ runtime DLL files that are only present on machines that have Visual Studio on them.

Always compile 64 bit binaries.

The DLL must be on all AE runtime machines, both for User Workspace and Web services components.

ModScript can load a DLL from an absolute path, but it is usually better to place the DLL in the folder that is specified in the "ScriptAppPath" entry in the Windows Registry. This topic is covered in more detail in the Composer help topic "Loading the Library in SBM ModScript".

The output is "5 : what : 12345". As you can see, ModScript passed two parameters to the DLL, the DLL can use those values to do whatever it needs to do, and it can also change those values to create output values that ModScript can use. As the parameters can be output, it is important that they are created before the call to "Lib.CallLibraryFunction()"; creating them inline creates const-temporary values that cannot serve as output.

Improvements in 11.4

I am not a big fan of Variant. I added it to ModScript in order to make it possible to convert AppScripts to ModScripts. However, I accidentally made it far more important than I intended, as many functions require that the Variant be created before being passed in, and many others have Variant as the return type. In 11.4, I created optional function signatures that do not require Variant. In this case, the Lib.CallLibraryFunction() now has an optional signature that takes string& for the parameters rather than Variant&. The documentation in 11.4 is much improved, adding the missing "Lib" class which is the underpinning class for this functionality.

ModScript uses the ChaiScript engine, which supports lambdas and has many algorithms built into it. These tend to go hand-in-hand. Lambdas are functions that do not have names. They can be passed like function pointers as parameters to other functions, or they can be directly invoked if desired. Algorithms often take a function and execute the function against a list of items, so lambdas are often useful in this context. They keep the logic you are invoking right in front of you, rather than requiring you to track down the function in order to understand what is happening. The great thing about using algorithms is that you don't have to write all the logic yourself.

In Part 2 of this series, I used the any_of() algorithm, along with a lambda, to check all records in an AppRecordList to see if any were still active. For reference:

With the algorithm, I can fit all that code inside my if statement. If you don't like my lambda, you could have defined the function elsewhere and used the function name (function names are function pointers, they can be passed as parameters to functions).

Ranges in ChaiScript

Before I can talk about algorithms, I need to discuss what a range is. A range is a tool for iterating a container. It consists of two iterators, one that points to the front of the container, and one that points to the back of the container. The idea is that you can move through the range forwards by popping the front or move through the range backwards by popping the back, all without changing the number of items in the underlying container. Vectors, Maps, strings, AppRecordLists, and Dictionaries all can be used as a "range" for iteration. The following functions can be invoked on a range:

bool empty()

Returns true if the range has no items in it.

void pop_front()

Moves the front iterator forward. The underlying container is not changed. If the range is empty, this throws an exception.

void pop_back()

Moves the back iterator backward. The underlying container is not changed. If the range is empty, this throws an exception.

front() : return type is the type of the item at the front of the container

Returns the item that the front iterator is pointing at. If the range is empty, this throws an exception.

back() : return type is the type of the item at the back of the container

Returns the item that the back iterator is pointing at. If the range is empty, this throws an exception.

Algorithms Available in ChaiScript

void for_each( container, func )

Iterates a range, executing a function on each item.

bool any_of( container, func )

Iterates a range, returns true if the function passed in returns true for any item in the range. "none_of()" could be created by simply invoking !any_of().

bool all_of( container, func )

Iterates a range, returns true if the function passed in returns true for all items in the range.

range find( container, valueToFind )

Returns a range with the front iterator pointing to the item found, or an empty range if the item was not found.

range find( container, valueToFind, compare_func )

Invokes compare_func on each item in the container, passing "valueToFind" and the current container entry; returns a range with the front iterator pointing to the first item for which compare_func returns true.

bool contains( container, valueToFind )

Returns true if any item in the container is equal to "valueToFind".

bool contains( container, valueToFind, compare_func )

Invokes compare_func on each item in the container, passing "valueToFind" and the current container entry; returns true if compare_func returns true.

Performs the function "func" over the container. Starts with "initial" and continues with each element. For example, "sum" invokes the `+` function on each item, starting with a value of 0.0 and returning the sum of items in the range.

double sum( container )

Returns the sum of items in the container. Items must be able to be added.

double product( container )

Returns the product of items in the container. Items must be able to be multiplied.

Returns a new container containing up to "num" elements copied from "container". AppRecordList cannot be used with this algorithm.

void take( container, num, inserterFunc )

Invokes "inserterFunc" with up to "num" elements from "container". For instance, the previous "take()" signature, which returns a new container, invokes this "take()" signature with the "back_inserter()" function which wraps the new container it will return; "back_inserter()" calls "push_back()" on the new container for each item passed to it. AppRecordList can be used with this algorithm as input, but "inserterFunc" should be something like back_inserter() wrapped around a Vector.

Returns a new container containing the first "n" elements copied from "container" for which "func()" returns true. AppRecordList cannot be used with this algorithm.

void take_while( container, func, inserterFunc )

Iterates container and invokes "func()" on each item until "func()" returns false. For the first "n" items for which "func()" returns true, invokes "inserterFunc()". For instance, the previous "take_while()" signature, which returns a new container, invokes this "take_while()" signature with the "back_inserter()" function which wraps the new container it will return; "back_inserter()" calls "push_back()" on the new container for each item passed to it. AppRecordList can be used with this algorithm as input, but "inserterFunc" should be something like back_inserter() wrapped around a Vector.

Returns a new container with values from "container" copied to it, skipping the first "num" items. AppRecordList cannot be used with this algorithm.

void drop( container, num, inserterFunc )

Invokes "inserterFunc()" with values from "container", skipping the first "num" items. AppRecordList can be used with this algorithm as input, but "inserterFunc" should be something like back_inserter() wrapped around a Vector.

Iterates container and invokes "func()" on each item until "func()" returns false. Returns a new container with all items copied from "container" starting where "func()" returned false. For example, "string::ltrim()" uses this to skip past white spaces in the string, copying the rest of the string into the return value; In Part 2 of this series, I wrote a custom trim function which trims commas off of the front and back of a string using "drop_while()". AppRecordList cannot be used with this algorithm.

void drop_while( container, func, inserterFunc )

Iterates container and invokes "func()" on each item until "func()" returns false. Invokes "inserterFunc()" with values from "container" starting where "func()" returned false. AppRecordList can be used with this algorithm as input, but "inserterFunc" should be something like back_inserter() wrapped around a Vector.

reduce( container, func ) : return type is the type of the first item in the "container" parameter

The "container" parameter must have at least two items in it. Initializes return value with the first item in "container", then iterates the remaining elements in container, invoking "func()" by passing in the current return value and the current element.

string join( container, delim )

Iterates container, invokes "to_string()" on each element and appends to the result string, with each entry separated by "delim".

Returns a new container with values copied from "container" for which "func()" returns true. AppRecordList cannot be used with this algorithm.

void filter( container, func, inserterFunc )

Iterates container, invokes "inserterFunc()" on each item for which "func()" returns true. AppRecordList can be used with this algorithm as input, but "inserterFunc" should be something like back_inserter() wrapped around a Vector.

Vector generate_range( iterator1, iterator2 )

For each item between "iterator1" and "iterator2" (inclusive), copy the values into the resulting Vector.

Vector generate_range( num1, num2 )

Creates a Vector with numeric values between "num1" and "num2", inclusive.

void generate_range( iterator1, iterator2, inserterFunc )

For each item between "iterator1" and "iterator2" (inclusive), invoke "inserterFunc()". AppRecordList can be used with this algorithm as input, but "inserterFunc" should be something like back_inserter() wrapped around a Vector.

void generate_range( num1, num2 , inserterFunc )

For each number value between "num1" and "num2" (inclusive), invoke "inserterFunc()".

Vector zip( collection1, collection2 )

Iterates collection1 and collection2 at the same time, returns a Vector where each entry is a Vector with the corresponding elements from collection1 and collection2. The resulting Vector will have the same number of elements as the smaller of the two collections. Example: zip( [1,2], [3,4,5] ) returns [ [1,3], [2,4] ].

Vector zip_with( func, collection1, collection2 )

Iterates collection1 and collection2 at the same time, returns a Vector where each entry is the return value from executing "func()" with the two corresponding elements from collection1 and collection2. The resulting Vector will have the same number of elements as the smaller of the two collections. Example: zip_with( `==`, [1,4,5,6], [0,4,5] ) returns [ false, true, true ].

void zip_with( func, collection1, collection2, inserterFunc )

Iterates collection1 and collection2 at the same time, invokes "func()" passing the corresponding elements from collection1 and collection2, passing the return value to "inserterFunc()" (inserterFunc() is often a "back_inserter()" on a Vector. For instance, the previous "zip_with()" signature creates a Vector to return, then invokes this "zip_with()" function, passing "back_inserter(myRetVect)" as the "inserterFunc".

reverse( container ) : return type is the type of the "container" parameter

Reverse the order of items in the container, returning a new container to the caller. AppRecordList cannot be used with this algorithm.

function bind( func, param1-n )

Creates an invoke-able function which can save some parameters passed at bind-time, along with some parameters passed at invoke-time, to the underlying function. Any parameter that is an underscore "_" implies that the resulting function will take a parameter, and the parameter passed to that function will be passed to the bound function.

For example, the implementation of "back_inserter( container )" is bind(push_back, container, _ );, meaning that the returned function takes a single parameter (just one underscore), when the returned function is invoked. Each call to the function returned from "back_inserter( container ) " will invoke push_back( container, XXX ), where XXX is the value passed to that function. FYI, in ChaiScript, a class method is the same as a function that takes an object of that class as the first parameter; as such, for a Vector v, there is no difference between v.push_back( x ) and push_back( v, x ).

Step by step: using a lambda that takes 3 parameters and creates a Vector out of them, "bind" will create a function that takes 2 parameters (see 2 underscores), and passes param1, the literal 3, and param2, to the lambda. Now, "b" will be a function that takes 2 arguments. We then invoke "b" with values 1 and 2, which results in a Vector of [ 1, 3, 2 ]. The "bind()" function can be very helpful when building a function pointer to pass to an algorithm.

function back_inserter( container )

Returns a function that takes a single parameter. Any time the function is invoked, the value passed as the parameter will be forwarded to a call to "container.push_back()".

min( v1, v2 ) : return type is the type of the "v1" parameter or the "v2" parameter, whichever is lower

If v1 < v2, v1 is returned; otherwise v2 is returned.

max( v1, v2 ) : return type is the type of the "v1" parameter or the "v2" parameter, whichever is higher

One useful feature of the ChaiScript engine is the support for JSON. As ModScript can be invoked directly via the Direct URL context, a JSON body could be sent by the caller and ModScript can parse it and then use it. Also, as ModScript can make REST call-outs, the fact that ChaiScript can format JSON for us empowers us to really get things done.

JSON is a convenient format for passing data in a web application as it is compact and well defined. Here is an example:

Parsing JSON

In the above JSON sample, the curly braces { ... } indicate the beginning of an object, with name-value pairs. The square braces [ ... ] indicate an array. When the ChaiScript engine parses a JSON string, integers become ints, floating points become doubles, text becomes strings. JSON arrays become Vectors. JSON objects become Maps which can store name-value pairs; it is important that the JSON not have repeating name entries in the same object as Maps can only store unique key values. With this in mind, the following is an ModScript excerpt that will parse the JSON from a string (as the JSON text has embedded quotes, we escape them with backslashes). The following script will write "Welcome to ModScript" as an Information entry in the Application Event Log:

We see a function call to Ext.LogInfoMsg(), which will take the output of the inner code and write it to the Application Event Log.

Inside, we see a string literal with the JSON embedded in it, with quotes escaped with backslashes. (Yes, string literals can have embedded newlines in them. The resulting string will have the newlines in the string).

After the string literal, we see a direct call to from_json(), which parses the JSON string into a Map (because the outer most part of the JSON is a JSON object).

The Map has the following keys: "arrayOfInts", "intData", "doubleData", "stringData"

The "arrayOfInts" entry in the Map is a Vector, and each entry in the Vector is an int.

The "intData" entry in the Map is an int, "doubleData" is a double, "stringData" is a string.

Finally, we see ["stringData"], which calls the lookup operator on the Map, returning the corresponding entry. In this case, it returns the string "Welcome to ModScript", which is what gets sent to the Ext.LogInfoMsg() function call.

Notes:

JSON objects can have entries that are objects. In this case, after a call to from_json() you will get a Map with a key-value entry where the value is a Map.

Entries in a JSON array do not all need to be the same type. A JSON array could have a text entry followed by an integer entry followed by an object entry. In this case, after a call to from_json() you will get a Vector with a string, an int, and then a Map. As such, the from_json() function can parse any JSON string, as long as the JSON objects do not have duplicate key names in them.

The top level of the JSON does not need to be an object, it can be an array, a string, an integer, or a double.

The return value of from_json() may be a Map, Vector, string, int, or double.

You may have noticed in Part 2 that we used from_json() to take a string with comma-separated integers and turn it into a Vector. When working with Multi-Relational, Multi-Selection, or Multi-Group fields, getting the internal value will return a comma-separated list of integers with commas at the beginning and end (example: ",65,732,899,"). Trim these commas, append square braces, and you have a JSON array (example: "[65,732,899]") which from_json() can parse into a Vector of integers.

Formatting JSON

The ChaiScript Engine also provides a to_json() function. This can be invoked on a Map, Vector, string, int, or double, and will generate a JSON string from the object. This is very helpful, especially when the scripter is pulling text from various locations, as they do not need to worry about encoding the quotes, etc, while building the JSON string. In a future blog article about using ModScript to invoke REST calls, you will see the creation of the HTTP message body that looks like this:

Formatting JSON From Variant

The to_json() utility does not play nicely with Variant. This is mainly because it is not clear what JSON type the Variant should be mapped to: is it a string, an int, a string that contains an int? Due to this ambiguity, there is no easy way to make Variant play nicely with to_json(). However, Variant has a member method Variant.to_string(). With this, you can get a string from the Variant, and string has other conversion methods like string.to_int(), so chaining a Variant "v" as such: v.to_string().to_int() will give you the value of the Variant as an int. SBM 11.4 introduced many methods to avoid Variant in general, and it also added functions like Variant.to_int().

We see a function call to Ext.LogInfoMsg(), which will take the output of the inner code and write it to the Application Event Log.

Inside, we see the beginning of an inline Vector

The first entry in the Vector will be the string text from the "TITLE" field.

The second entry in the Vector will be the integer value (internal value) of the "STATE" field.

The to_json() function is invoked on the Vector, creating a JSON string.

The output JSON is sent to the call to Ext.LogInfoMsg().

An example of the output: ["Test item 1", 27]

Notes:

to_json() does not work with Variant directly (or a Map/Vector which contains a Variant). A Variant can be converted to a string using Variant.to_string(), and the returned string can then be converted to int using string.to_int(). SBM 11.4 added functions like Variant.to_int() and Variant.to_double() so that the value does not first need to be converted to a string.

The output text from to_json() has newlines and white-space, which makes the JSON readable.

SBM ModScript uses the ChaiScript engine. ChaiScript is a fast, modern scripting engine that was written in C++. As it was written by a C++ developer, it has a bit of a C++ flavor to it. For instance, the growable array class is named Vector and is, in-fact, implemented using the C++ std::vector container. In some ways, this may make it easier to identify what the methods for the classes in ChaiScript will look like.

However, ChaiScript does not expose every C++ function from a given class into the scripting language. For instance, the string class does not include the string.replace() function. Nevertheless, ChaiScript DOES let you extend a class to add more functions to it. As such, we can write the "missing" function in a ModScript, then include() that script in other scripts. It is a "best practice" to build a library of helpful utilities.

In part 1 we reviewed what ModScript is. Let's start looking at some examples:

In Use Case 1, the ModScript will run in the Post Transition context of an item's Close transition. The item might be selected in a multi-relational field on a separate item, and if all items in that multi-relational field are closed, we'll transition the container item. This is similar to what can be done with the SBM Sub-Tasks feature, but we'll do it via scripting because there could be some extra logical detail that Sub-Tasks couldn't check for but scripting could (and it gives us a showcase for ModScript features). The full application can be found here: Container.zip. This script uses the algorithms "drop_while()" and "any_of()", to see more information on algorithms, see Part 5. This script also uses "from_json()", to see more about ChaiScript's JSON utility functions, see Part 4.

// set up some constants for use later in the script
add_global_const( "USR_CONTAINER", "CONTAINER_TBL_NAME" );
add_global_const( "RELATED_ITEMS", "CONTAINER_FIELD" );
// Get the container application table id and add it as a global
global CONTAINER_TBL = Ext.TableId(CONTAINER_TBL_NAME);
// define a function that trims commas and returns the new string
def TrimCommas( s ) {
/* ChaiScript engine automatically returns whatever occurs on the last line of a
function. We could add a "return" statement here if desired for clarity.
This may be a little hard to follow, but we call drop_while on our string to remove
the starting commas, then the return value is reversed, we do drop_while again to
trim the back, then reverse again.
These "fun" statements are anonymous functions known as lambdas */
reverse( drop_while( reverse( drop_while( s, fun(x){ return x == ','; } ) ),
fun(x) { x == ','; }));
}
// Find the multi-relational field
var relational = Ext.CreateAppRecord( Ext.TableId("TS_FIELDS"),
FieldTypeConstants.MULTIPLE_RELATIONAL );
relational.ReadByColumnAndColumn("TABLEID", CONTAINER_TBL, "DBNAME", CONTAINER_FIELD );
// Create a list, as it is possible that our item is in more than one container item
var containerList = Ext.CreateAppRecordList( CONTAINER_TBL );
/* Yes, ModScript has multi-line comments!
Read the list of items that contain this item.
Use SQL binding by passing a Vector of Pair objects, each with a data type and a value.
Vectors can be created on the fly using [ ... ] syntax */
containerList.ReadWithWhere(
"TS_ID in (select TS_SOURCERECORDID from TS_USAGES where TS_FIELDID=? and TS_RELATEDRECORDID=?)",
[ Pair(DBTypeConstants.INTEGER, relational.GetId()),
Pair(DBTypeConstants.INTEGER, Shell.Item().GetId()) ] );
// loop through the resulting list, using the "for each" syntax (just a ":")
for( containerItem : containerList ) {
// for each item, read the contents of the relational field, check the items,
// transition if necessary
// Get the field value from the containing item. GetFieldValue() returns a Variant,
// so use to_string() to get it as a string.
// In 11.4, we have GetFieldValueString(), GetFieldValueInt(), etc, for getting field
// values as the desired type.
var fieldVal = containerItem.GetFieldValue(CONTAINER_FIELD).to_string();
// remove the current item from the comma-separated list of items
var regex = Regex();
regex.Compile( ",${Shell.Item().GetId()}," );
fieldVal = regex.ReplaceAll( fieldVal, "," );
// trim outside commas off of list
fieldVal = TrimCommas( fieldVal );
// turn comma separated list into Vector of values.
// ChaiScript can do this with "from_json" if we use array syntax [ ... ]
// ChaiScript has in-string processing, use ${ ... } inside a string and the stuff
// inside the braces will be processed by the engine and put inline in the string.
var itemIDs = ("[${fieldVal}]").from_json();
// Loop through those other contained items to see if they are inactive too.
// We could do another for-each loop here, but let's use an algorithm instead.
if ( !any_of( itemIDs,
fun( itemID ) { // return true if item is active
var contained = Ext.CreateProjectBasedRecord( CONTAINER_TBL );
return contained.Read(itemID) &&
contained.GetFieldValue( "ACTIVEINACTIVE" ) == 0;
} ) ){
// no items found that are active, we need to transition this container item
containerItem.QuickTransition( "CONTAINER.CLOSE", true );
}
}

SBM has had a scripting engine for two decades now called AppScript. It is powerful and fast. It is also based on VBScript, and was crying out for a refresh. SBM gained a new scripting engine in 11.3 named ModScript. AppScript is still supported.

So, what is ModScript? ModScript is based on the ChaiScript engine. When reading about ChaiScript, it is important to separate in your mind the documentation meant for C++ developers from the documentation about the scripting language. The C++ commentary and documentation is there for the implementers of languages like ModScript, not for you, the consumers of the scripting language. In order to get some visibility into ChaiScript, you can peruse the unit tests and samples, again, be sure to view ".chai" files, not ".hpp" or ".cpp" files. However, this series will have example scripts, so you don't necessarily need to dive too deeply into those long lists of files at the moment. For now, know that ChaiScript is a modern style script language with keywords like "for", "while", "continue", "break", "if", "else", "switch", etc, and it allows the script writer to create functions, classes, variables, global variables, global constants, function pointers, lambdas, and more. Much of the syntax is similar to JavaScript; however, the variables are strongly typed (once they are assigned a value of type "int", they cannot become a "string"). ChaiScript also has the concept of a Vector (array which can grow) and a Map (a binary search tree which stores key-value pairs, the key is a "string"). Furthermore, it has many utility functions including to_json() and from_json(), which know how to interact with Vector, Map, string, int, and double data types to create and parse valid JSON.

ModScript is more than ChaiScript; a scripting engine can't do much without an API for interacting with the host program. We added all the functionality of AppScript and more. We also created a conversion tool that converts scripts from AppScript to ModScript. As we already had a scripting engine in place, we were able to piggyback off of AppScript to create identical integration points for ModScript:

Pre-Transition

Post-Transition

Pre-State

Post-State

Notification

Self Registration

HTML Template

Direct URL

Database Import.

SOAP - In 11.4, we add a SOAP entry point, allowing ModScripts to be invoked by SOAP callers (including the Orchestration Engine and the new Scheduler).

With all these integration points, scripts can be invoked in nearly all aspects of SBM. In fact, developers who are familiar with AppScript Contexts will find that the ModScript runtime contexts are identical, providing the same information and access. All of AppScript's Ext functions are available in ModScript, such as Ext.CreateAppRecord() and Ext.TableId(). All of AppScript's classes are available, such as AppRecord, VarRecord, ProjectBasedRecord, and User, and all those classes have the same (or more) methods available in ModScript.

What we added to ModScript, above and beyond the functionality we copied from AppScript, is what I am very excited about:

Time zone awareness

Locale awareness

Date/time string formatting and parsing using time zone and locale rules

Regular expressions

REST call-outs (improved in 11.4)

Logging to files

Logging to Active Diagnostics (11.4)

ModScript can execute transitions on items from primary tables (AppScript could only Update, not Transition).

ModScript is available in a limited, read-only capacity in SaaS, as it is written from the ground up to be namespace safe.

Not only is ModScript feature rich, the features are growing. Almost every question from early adopters has lead to an enhancement to ModScript. For instance, a question about how to generate a temporary file name using the current time led the the creation of the TimeMillis class, which can interact with time down to the millisecond, as will as the TempFile class, which creates a temporary file and provides the file path name (the temp file gets deleted when the TempFile object goes out of scope). These enhancements are coming in 11.4, along with the ability to format and parse date/time strings using custom formats and more.

As an example of the features available in ModScript, consider REST interaction. ModScript can handle direct call-ins via the direct URL usage, and the script has the HTTP body available to it, making it possible to POST data to ModScript as JSON and have ModScript parse it, act on it, and write JSON back to the caller. In addition to that, ModScript can invoke REST calls using GET and POST, with all other HTTP verbs available in 11.4. ModScript gives you the ability to publish a single REST endpoint from Composer, which can be customized per-runtime in AR, and ModScript can modify the URL path and URL parameters so that one endpoint can serve any number of REST entry points from a REST API. It can send custom headers and see the HTTP headers from the response (11.4). As it can use the SBM Proxy, ModScript call-outs can take advantage of the OATH2 token exchange in order to fully support OATH2 end points. Opening up REST in this way completely opens the doors of what is possible.

From the hints above, it is clear that ModScript is still growing. Many features were added in 11.4 to extend functionality, improve usability, and fill gaps. The documentation coming in 11.4 is an overhaul of what was available previously, making the functionality much more understandable. We included many more examples, and corrected bad examples, to help get you going. Also in 11.4, the Composer Validate button becomes useful again, checking for missing variable/function declaration (which helps catch misspelled variable/function names). We are dedicated to helping you be successful with scripting and removing any frustration from the SBM experience.

Logging values of variables and object properties is a very useful tool. SBM has interfaces such as Ext.LogInfoMsg() functions that are useful in logging messages to the Windows Event Viewer Logs. I find it a little nicer to avoid filling up my Windows log and instead log to a separate file.

In comes the function Ext.AppendTextFile(filename, messageString). It will write a message to a specific file. It is import to remember that the IIS app pool identity credentials are used to write to this file. For this reason, I like to store my log file in the SBM Application Engine directory which requires the identity user to have write access to it. The following line of code will write a message to "scriptlogging.txt" file.

Popular

When uploading files into Dimensions CM, the only valid character is the hyphen. But when uploading 30000+ files it can be like finding a needle in the haystack as to why your upload fails.
After wai...