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.