TypeScript SDK

TypeScript SDK — это библиотека для работы с системой из пользовательских сценариев.

Что такое сценарии

Сценарии системы — это мощный и гибкий инструмент настройки системы под нужды компании. С помощью сценариев можно имплементировать сложную логику работы с объектами системы. Сценарий представляется собой набор функций, написанных на языке TypeScript. Каждый сценарий имеет контекст запуска, а также доступ к другим глобальным константам.

Где можно использовать сценарии

Сценарии могут исполняться на серверной стороне сервиса или в браузере пользователя. Функционал серверных и клиентских сценариев в настоящее время идентичен с одним отличием: во время исполнения сценариев в браузере они ограниченны правами пользователя, в браузере которого они выполняются. Серверные же сценарии выполняются с правами администратора системы.

Серверные

Сценарии доступны в бизнес-процессах как отдельный шаг Сценарий. В качестве контекста таких сценариев выступает экземпляр процесса.

Важно: исполнение серверных сценариев в облачном решении ограничено по времени. Если сценарий не завершится в течение 1 минуты, то он прерывается.

Клиентские

Клиентские сценарии на данный момент доступны при создании страниц с помощью Виджетов.

Что можно сделать с помощью сценариев

С помощью сценариев можно создавать, запрашивать, изменять и удалять элементы приложений, а также взаимодействовать с внешними системами по протоколу HTTP.

async function createOrder() {
    // Корзина заказа
    const items = await Context.fields.items.fetchAll();
    // Стоимость корзины
    const total = items
        .map(item => item.price)
        .reduce((acc, price) => acc.add(price));
    // Создадим заказ
    const order = Context.fields.order.app.create();
    order.data.agent = Context.data.__createdBy;
    // Применим скидку
    order.data.total = total.multiply(0.75);
    // Сохраним заказ
    await order.save();
    Context.data.order = order;
    // Отправим идентификатор заказа на сайт
    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,
        }),
    });
}

Принципы работы

Сценарий представляет из себя файл с кодом на TypeScript. Работа над сценарием производится во встроенном редакторе, основанном на Monaco, в котором добавляются файлы определения TypeScript SDK для автодополнения. При публикации процесса или виджета сценарии валидируются и транспилируются в JavaScript, который и будет исполняться в дальнейшем.

Во время исполнения создаётся песочница, в которой выполняется весь файл сценария, после чего вызывается конкретная функция. В процессах сценарий выполняются для каждого шага. В виджете сценарий выполняется один раз при загрузке виджета, после чего выполняются уже только отдельные функции.

Асинхронное выполнение (async / await)

Важным моментом при работе сценариев является то, что многие операции являются асинхронными: получение элемента по ссылке, сохранение элемента, работа с внешними сервисом. Такие методы возвращают Promise. Promise (в переводе с англ. «Обещание»), является обещанием того, что мы получим результат нашей операции. Если же нам необходим результат нашей операции для совершения других действий, то нужно дождаться этого «обещания». Для лучшей читаемости кода, мы рекомендуем использовать синтаксис async/await.

Синтаксис:

  • async обозначает, что в функции будут происходить асинхронные операции
  • await гарантирует, что promise («Обещание»), будет выполнено и мы получим результат нашей асинхронной операции

Рассмотрим пример получения из переданных в контексте пользователей текущего юзера по указанному email:

// Ставим перед нашей функцией async, обозначая, что в ней будут происходить асинхронные операции
async function getUsers(): Promise<UserItem> {
    // Метод fetchAll является асинхронным методом, поэтому ставим await, чтобы дождаться результата
    // Если не дождаться результата, то скрипт будет выполняться дальше
    // и вернет нам объект типа Promise, а не массив пользователей
    const users = await Context.fields.users.fetchAll();
    const currentUser = users.find((user: UserItem) => user.data.email === Context.data.email);
    return currentUser;
}

Обратите внимание, что использование ключевого слова await необязательно. Если его пропустить, то вызываемая функция продолжит выполнение, но вы уже не сможете узнать результат выполнения. Это может быть полезно при отправке логов или статистики во внешний сервис:

async function getUsers(): Promise<UserItem> {
    const users = await Context.fields.users.fetchAll();
    
    // В этом случае мы не ждем ответа сервера и никак не обрабатываем ошибки,
    // так как для нашей логики выполнение этого метода не имеет критичного значения
    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;
}

Массовая обработка (Promise.all)

Promise.all преобразует массив «обещаний» в «обещание» массива результатов, другими словами, позволяет дождаться, пока все promise, переданные в эту конструкцию, разрешатся, и возвращает массив результатов их выполнения.

Допустим, нужно скачать несколько файлов со сторонних ресурсов:

async function loadFiles(): Promise<Array<ApplicationItem<T>>> {
    // Получаем массив праметров для загрузки файлов типа Приложение
    const filesParams = await Context.fields.filesParams!.fetchAll();
    // Посредством метода массива map получаем массив промисов
    const promises = filesParams.map(async (fileParams: Array<ApplicationItem<T>>) => {
        // В соответствующие переменные записываем соответствующие значения массива,
        // которые возвращает Promise.all
        const [name, link] = Promise.all([
            await fileParams.data.name!.fetch(),
            await fileParams.data.link!.fetch()
        ]);
        // Создаем файл в контексте процесса с именем по индексу
        Context.fields.file.createFromLink(`${ name }`, `${ link }`);
    });
    // Для получения загруженных файлов в конструкцию Promise.all помещаем полученный массив промисов
    // Дожидаемся получения результата
    const files = await Promise.all(promises);
    // Возвращаем массив загруженных файлов для дальнейшего использования
    return files;
}

Если надо получить данные несколькими запросами, и при этом нет необходимости ждать результаты друг друга, рекомендуется распараллеливать такие запросы через метод Promise.all().

Например, до оптимизации (запросы будут выполняться последовательно и каждый последующий ждет окончания выполнения предыдущего):

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

После оптимизации (запросы выполнятся параллельно):

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

Также можно использовать для параллельного массового сохранения элементов приложений:

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

Non-Null Assertion Operator (!)

Это постфикс оператор, который в дословном переводе означает «оператор ненулевого утверждения», то есть при установке данного оператора после выражения мы утверждаем, что данное выражение не может быть null и undefined. В связи с этим пользоваться ! при моделировании решения нужно осторожно, зная, что к моменту выполнения скрипта поле точно не будет null или undefined, иначе скрипт упадет на этой строчке.

// Посмотрим на стандартный пример получения элемента приложения.
// Конструкция app! означает, что мы даем компилятору гарантию, что значение `app` не может быть `null` или `undefined`, 
// и можно его использовать
const app = await Context.data.app!.fetch();
app.data.file = Context.data.file;
await app.save();
// Если есть вероятность, что поле будет `null` или `undefined` к моменту выполнения скрипта,
// лучше добавлять проверки с помощью условий
deal.data.outstanding = (deal.data.contract_amount || new Money(0, 'RUB').add(deal.data.received_payments.multiply(-1));
// Без проверки такая строчка была бы написана вот так:
deal.data.outstanding = new Money(deal.data.contract_amount!.asFloat() - deal.data.received_payments.asFloat(), 'RUB' );

Elvis Operator (?.)

Оператор опциональной последовательности ?. позволяет получить значение свойства, находящегося на любом уровне вложенности в цепочке связанных между собой объектов, без необходимости проверять каждое из промежуточных свойств в ней на существование. ?. работает подобно оператору ., за исключением того, что не выбрасывает исключение, если объект, к свойству или методу которого идёт обращение, равен null или undefined. В этих случаях он возвращает undefined.

Таким образом, мы получаем более короткий и понятный код при обращении к вложенным по цепочке свойствам объекта, когда есть вероятность, что какое-то из них отсутствует.

// Посмотрим на стандартный пример получения элемента приложения
// Конструкция `Context.data.app?.fetch()` вернет `undefined` если само значение `app` пустое, 
// а значит, мы можем упростить код проверки
const appItem = await Context.data.app?.fetch();
if (appItem) {
    // Тут уже точно `appItem` существует
}

Структура справки

Справка состоит из следующих разделов:

  • Типы данных — раздел, посвящённый базовым типам системы.
  • Типы объектов — раздел, в котором описываются интерфейсы объектов, доступные из сценариев.
  • Глобальные константы — список глобальных констант, доступных в рамках сценариев.
  • Работа с приложениями — раздел, в котором рассматриваются способы работы с элементами приложений, такие как создание, поиск, изменение и удаление.
  • Работа с внешними сервисами — описание функции fetch, с помощью которой можно отправлять HTTP-запросы во внешние системы.