TypeScript SDK

TypeScript SDK is a collection of objects and methods for scripting in BRIX.

What is a script

System scripts are a powerful tool for adjusting the system to the company’s needs. They allow you to implement complex logic for working with system objects. A script is a set of functions written in TypeScript. Every script has a launch context and access to other Global constants.

Where scripts can be used

Scripts can be run both on the server-side and in the client browser. At the moment, the server and client scripts work in the same with just one difference: when scripting in a browser, the scripts are limited by the permissions of the user in whose browser they run. Server-side scripts can be run only with the administrator permissions.

Server-side scripts

Scripts can be used in a business process through the Script activity. The process instance serves as the context for such scripts.

Important: Server-side scripts are time-limited in the SaaS edition of the system. If the script execution is not completed within 1 minute, it is aborted.

Client scripts

Client scripts can be used when building pages with widgets.

What can be done with scripts

With scripts we can create, request, adjust and delete app items, and interact with external systems through the HTTP protocol.

async function createOrder() {
    // Shopping cart
    const items = await Context.fields.items.fetchAll();
    // Total cost
    const total = items
        .map(item => item.price)
        .reduce((acc, price) => acc.add(price));
    // Create order
    const order = Context.fields.order.app.create();
    order.data.agent = Context.data.__createdBy;
    // Apply discount
    order.data.total = total.multiply(0.75);
    // Save order
    await order.save();
    Context.data.order = order;
    // Send order ID to site
    await fetch(`https://my-store.ru/orders/${ Context.data.storeId }`, {
        method: 'PATCH',
        headers: {
            Authorization: 'bearer MY-SECRET-TOKEN',
        }
        body: JSON.stringify({
            QBPMOrderId: order.data.__id,
        }),
    });
}

How it works

A script is a file with TypeScript code. For working with scripts, you will use a built-in editor based on Monaco, where you can add TypeScript SDK definition files for autocompletion. When publishing a process or a widget, the scripts are validated and converted to JavaScript that will be run later.

While running, a sandbox is created where the entire script file is executed, and after that a certain function is called. In processes, a script is executed for each step. In a widget, a script runs once when the widget is loading, then only single functions are executed.

Asynchronous execution (async/await)

When working with scripts, keep in mind that many operations are asynchronous: getting an item by URL, saving an item, working with an external service. These methods return Promise. Promise guarantees getting the result of the operation. In case we need the result of the operation in order to execute other activities, we need to wait for the Promise. For better code readability, we recommend using syntax async/await.

Syntax:

  • async means that asynchronous operations will run in the function.
  • await ensures that the Promise will be fulfilled, and we will obtain the result of the asynchronous operation.

Let’s take a look at the following case of getting the current user by indicated email from the list of all the users transferred in the context:

// Put async ahead of our function, defining that asynchronous operations will be run
async function getUsers(): Promise<UserItem> {
    // The `fetchAll` method is an asynchronous method, so we put in `await` to wait for the result
    // If we don’t wait for the result, the script will continue to run 
    // So, the script will return a Promise type object, not an array of users
    const users = await Context.fields.users.fetchAll();
    const currentUser = users.find((user: UserItem) => user.data.email === Context.data.email);
    return currentUser;
}

Important: Using await is not mandatory. If it is ommitted, the called function will continue to run, and you won’t be able to learn the result of its execution. This can be used when you need to send logs or statistics to an external service:

async function getUsers(): Promise<UserItem> {
    const users = await Context.fields.users.fetchAll();
    
    // In this case, we don’t wait for the server response, and errors are not handled,
    // as the execution of this method is not critical for the logic of the script
    fetch('https://my-log-server/?log=' + `getUsers:users_count = {users.length}`);

    const currentUser = users.find((user: UserItem) => user.data.email === Context.data.email);
    return currentUser;
}

Multiple operations (Promise.all)

Promise.all converts an array of Promises into a Promise of the result array. In other words, it allows you to wait for all the Promises transferred into this structure to be resolved, and, after that, it returns the array of execution result.

Let’s assume that we need to download several files from external resources:

async function loadFiles(): Promise<Array<ApplicationItem<T>>> {
    // Obtain an array of parameters to download files of app type
    const filesParams = await Context.fields.filesParams!.fetchAll();
    // Using the map array method, obtain a promises array
    const promises = filesParams.map(async (fileParams: Array<ApplicationItem<T>>) => {
        // Write the array values returned in `Promise.all`
        // into the corresponding variables
        const [name, link] = Promise.all([
            await fileParams.data.name!.fetch(),
            await fileParams.data.link!.fetch()
        ]);
        // The file with an index-based name is created in the process context
        Context.fields.file.createFromLink(`${ name }`, `${ link }`);
    });
    // Put the obtained promises array into `Promise.all` to get the downloaded files
    // Wait for the result
    const files = await Promise.all(promises);
    // Return the array of downloaded files for further use
    return files;
}

If you need to obtain data using several requests, and there is no need to wait for the result of each one, we recommend executing these requests in parallel using the Promise.all() method.

For example, here is a script before optimization (all requests are executed sequentially, each one waiting for the result of the previous one):

const teams = await Namespace.app.teams.search().all();
const areas = await Namespace.app.responsible_groups.search().all();
const issues = await Namespace.app.issue.search().all();

Here is this script after optimization (the requests run in parallel):

const [teams, areas, issues] = await Promise.all([
    await Namespace.app.teams.search().all(),
    await Namespace.app.responsible_groups.search().all(),
    await Namespace.app.issue.search().all(),
]);

You can also use this to save multiple app items simultaneously:

await Promise.all(teams.map(t => t.save()))

Non-Null Assertion Operator (!)

This is a postfix operator. When placing it after the expression statement, we confirm that this statement cannot be null and undefined. In this regard, you need to be careful in using ! when modeling the solution, and should be aware that by the moment the script runs, this field can’t be null or undefined, otherwise the script will fail at this line.

// Let’s consider a standard case of obtaining an app item
// The `app!` structure means that we guarantee `app` is not `null` or `undefined`
// so it can be used in the script
const app = await Context.data.app!.fetch();
app.data.file = Context.data.file;
await app.save();
// If there is a possibility that the field is `null` or `undefined` by the time the script is executed,
// then it is better to add condition checks
deal.data.outstanding = (deal.data.contract_amount || new Money(0, 'EURO').add(deal.data.received_payments.multiply(-1));
// Without a check, this line would be written like this:
deal.data.outstanding = new Money(deal.data.contract_amount!.asFloat() - deal.data.received_payments.asFloat(), 'EURO' );

Elvis Operator (?.)

The Optional chaining operator ?. allows you to get the value of a property located at any nesting level in a chain of interconnected objects, without having to check each of the intermediate properties in it for existence. ?. works like the . operator, except that it doesn’t throw an exception if the object whose property or method is being accessed equals null or undefined. In these cases it returns undefined.

This way, we get shorter and clearer code for accessing nested object properties when there is a possibility that one of them is missing.

// Let’s take a look at a standard example of obtaining an app item
// `Context.data.app?.fetch()` returns `undefined` if `app` is empty, 
// so it is possible to make the check code simpler
const appItem = await Context.data.app?.fetch();
if (appItem) {
    // Here, we know for sure that `appItem` exists
}

Help Center layout

Help Center includes the following sections:

  • Data types. This section is dedicated to basic system data types.
  • Object types. This section describes object interfaces available in scripts.
  • Global constants. A list of global constants available in scripts.
  • Work with apps. This section describes how to create app items, search for them, edit them, and delete them.
  • Web requests. Description of the fetch function used to send HTTP requests to external systems.