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-запросы во внешние системы.