Перейти к основному содержимому
Версия: 4.x

Обращение из внешней системы

Вызов действий (Action API)​

Платформа предоставляет возможность внешним системам обращаться к разработанной на lsFusion системе с использованием различных сетевых протоколов. Интерфейсом такого взаимодействия является вызов некоторого действия с заданными параметрами и, при необходимости, возврат значений некоторых свойств (без параметров) в качестве результатов. Предполагаются, что все объекты параметров и результатов являются объектами встроенных классов.

Задание действия​

Вызываемое действие может задаваться одним из трех способов:

  • EXEC - задается имя вызываемого действия.
  • EVAL - задается код на языке lsFusion. Предполагается, что в этом коде присутствует объявление действия с именем run, именно это действие и будет вызвано.
  • EVAL ACTION - задается код действия на языке lsFusion. Для обращения к параметрам можно использовать спецсимвол $ и номер параметра (начиная с 1).

Протоколы​

На данный момент в платформе поддерживаются следующие сетевые протоколы:

HTTP​

Взаимодействие по этому протоколу поддерживается как с сервером приложений на порту 7651, так и с веб-сервером (при наличии такового), на том же порту, на котором установлен веб-клиент.

Формат URL, в зависимости от способа задания действия, выглядит следующим образом:

  • EXEC - http://адрес сервера:порт/exec?action=<имя действия>. Параметр action всегда должен быть задан.
  • EVAL - http://адрес сервера:порт/eval?script=<код>. Если параметр script не задан, то предполагается, что код передается первым параметром BODY.
  • EVAL ACTION - http://адрес сервера:порт/eval/action?script=<код действия>. Если параметр script не задан, то предполагается, что код передается первым параметром BODY.
Параметры​

Параметры могут передаваться как в строке запроса (добавлением в ее конец строк формата &p=<значение параметра>), так и в теле запроса (BODY). При этом предполагается, что для выполняемого действия, сначала подставляются параметры URL (в порядке их следования в запросе), а только потом параметры BODY.

При обработке параметров BODY, параметры с типом контента из следующей таблицы считаются файлами, и передаются в параметры действия в виде объектов файлового класса (FILE, PDFFILE и т.п.). При этом в расширение файла записывается соответствующее расширение из упомянутой таблицы. Если тип контента отсутствует в этой таблице, но начинается на application, то параметр все равно считается файлом, а в расширение этого файла записывается правая часть типа контента (например для типа application/abc в расширение файла записывается abc). Параметры с типом контента application/null считаются равными NULL.

Параметры BODY с типами контента, отличными от вышеупомянутых, считаются строками, и при вызове автоматически преобразуются к классам параметров вызываемого действия. Пустые строки при этом преобразуются в NULL.

Заголовки выполняемого запроса автоматически сохраняются в свойство System.headers[TEXT]. Так, в единственный параметр этого свойства записывается название заголовка, а в значение свойства - значение этого заголовка.

Результаты​

Свойства, значения которых необходимо вернуть в качестве результата, передаются в строке запроса, добавлением в ее конец строк формата &return=<имя свойства>. При этом предполагается, что значения указанных свойств возвращаются в порядке их следования в строке запроса. По умолчанию, если ни одно свойство результата не задано, результирующим свойством считается первое свойство с не NULL значением из следующего списка.

Если результат запроса является файлом (FILE, PDFFILE и т.п.), то тип контента ответа, в зависимости от расширения файла, определяется в соответствии со следующей таблицей. Если расширение файла отсутствует в этой таблице, тип контента устанавливается равным application/<расширение файла>.

Расширение файла при этом определяется автоматически по аналогии с оператором WRITE.

Во всех трех верхних случаях, если значение результата равняется NULL, то вместо расширения файла в тип контента подставляется строка null (например application/null), а в качестве самого ответа возвращается пустая строка.

Результаты запроса, отличные от файловых, преобразуются к строкам и передаются с типом контента text/plain. NULL значения возвращаются как пустые строки.

Значения свойства System.headersTo[TEXT] автоматически записываются в заголовки результата запроса. Так, из единственного параметра этого свойства читается название заголовка, а из значения свойства - значение этого заголовка.

Несколько результатов / параметров в BODY​

Если BODY запроса имеет тип multipart/* или application/x-www-form-urlencoded, то он разбирается на части и каждая из этих частей считается отдельным параметром запроса. При этом порядок этих параметров совпадает с порядком соответствующих частей в BODY запроса.

В свою очередь, если количество возвращаемых результатов больше одного, то:

  • Если в запросе есть параметр returnmultitype=bodyurl - тип контента ответа при передаче устанавливается равным application/x-www-form-urlencoded, а результаты кодируются так, как если бы они передавались в строке запроса.
  • Иначе - тип контента ответа при передаче устанавливается равным multipart/mixed, а результаты передаются как составные части этого ответа.
к сведению

Отметим, что обработка параметров и результатов http запроса во многом аналогична их обработке в обращении к внешней системе по протоколу HTTP (параметры при этом обрабатываются как результаты, и, наоборот, результаты обрабатываются как параметры)

Stateful API​

Описанный выше API, по умолчанию, является REST API. Соответственно, сессия изменений создается в момент вызова, а по окончанию этого вызова сразу же закрывается. Однако, в некоторых случаях такое поведение нежелательно, и необходимо накапливать изменения в течении некоторого промежутка времени (например, в процессе ввода информации пользователем), а значит, сессию необходимо сохранять и передавать между вызовами. Для того, чтобы сделать это, при выполнении запроса в конец строки запроса можно добавить строку формата &session=<идентификатор сессии>, где <идентификатор сессии> - любая не пустая строка. В этом случае, сессия по окончанию вызову не закроется, а привяжется к переданному идентификатору, и все последующие вызовы с этим идентификатором, будут выполняться в этой сессии. Для того, чтобы закрыть сессию (по окончанию вызова), к ее идентификатору в строке запроса необходимо добавить постфикс _close (например &session=0_close).

к сведению

Так как для работы с HTTP сессиями неявно используются cookie, важно не забывать сохранять / передавать cookie между stateful http-вызовами (впрочем, как правило, это делается автоматически, например браузером, HttpClient в Java и т.п.)

к сведению

В текущей реализации платформы при использовании сессий элементы системы (например локальные свойства) созданные в текущем вызове удаляются, то есть в последующих вызовах не видны.

Аутентификация​

При выполнении http-запроса часто бывает необходимо идентифицировать пользователя, от чьего имени будет выполняться заданное действие. На данный момент в платформе поддерживается два типа аутентификации:

  • Базовая аутентификация - в заголовке Authorization: Basic <credentials> в кодированном виде передаются пользователь и пароль.
  • Аутентификация на базе токена - состоит из двух этапов:
    • На первом этапе необходимо выполнить действие Authentication.getAuthToken[] c базовой аутентификацией. Результатом этого действия будет токен аутентификации с фиксированным временем действия (по умолчанию один день). Пример запроса: http://localhost/exec?action=getAuthToken.
    • Далее полученный токен можно использовать для аутентификации в течении периода его действия, передавая его в заголовке Authorization: Bearer <token> (по аналогии с JWT, который и используется в текущей реализации платформы для генерации токенов аутентификации)

Работа с формами (Form API)​

Кроме выполнения действий, в платформе также поддерживается API (в стиле JSON API) по работе с формами, а точнее с их интерактивным представлением. Так как это stateful API и оно спроектировано для работы в асинхронном режиме (а значит непосредственно в HTTP-интерфейсе есть ряд системных параметров, вроде индекса запроса, индекса последнего полученного ответа и т.п.), этот API удобнее использовать с помощью специальных библиотек для конкретных языков / платформ, с которыми необходима интеграция:

Протоколы​

JavaScript​

Библиотека работы с JavaScript доступна в центральном npm-репозитарии под именем @lsfusion/core.

Ключевым понятием в этом API является понятие состояния. Под состоянием подразумевается js-объект, структура которого соответствуют элементам формы следующим образом:

  • Группа объектов соответствуют js-объекту, которое хранится в поле js-объекта состояния. Имя этого поля совпадает с именем группы объектов. Каждый js-объект группы объектов, в свою очередь, в поле list хранит массив js-объектов (с учетом заданных фильтров и порядков). Сам js-объект группы объектов соответствует текущему набору объектов. Также, каждый js-объект массива (как и сам js-объект группы объектов) в поле value хранит значения объектов - просто значение, если объект в группе объектов один, или, если объектов несколько, js-объект с полями, имена которых совпадают с именами объектов, а значения - со значениями объектов.
  • Свойства соответствуют значению, которое хранится в поле (имя этого поля совпадает с именем свойства) js-объекта, который в зависимости от наличия параметров и представления определяется следующим образом:
    • У свойства есть параметры:
      • Представление свойства равно GRID - каждого js-объекта массива list js-объекта группы отображения этого свойства.
      • Представление свойства равно PANEL, TOOLBAR - js-объекта группы отображения этого свойства
    • У свойства нет параметров - js-объекта состояния.

Соответственно задача библиотеки автоматически поддерживать описанное выше состояние актуальным, как при создании формы, так и при ее последующем изменении (часто такое поведение называют реактивностью).

Библиотека экспортирует следующие функции:

  • create - создает новую форму. Параметры:
    • setState - функция запроса изменения состояния. Эта функция, должна принимать на вход один параметр - функцию изменения состояния (у которой, в свою очередь один параметр - предыдущее состояние, а результат - следующее состояние), и в результате выполнения добавлять эту функцию в очередь изменения состояний (или, к примеру, применять сразу в зависимости от реализации логики представления). Такая логика работы с состояниями полностью соответствует аналогичной логике работе с состояниями в React, и, как правило, при использовании внутри React-компонента параметр setState передается равным: updateState => this.setState(updateState).
    • baseUrl - url веб-сервера lsFusion - строка, например 'https://demo.lsfusion.org/hockeystats'.
    • formData - объект описывающий форму. Должен содержать либо поле name с именем формы (например { name: "MainForm"}), либо поле script с кодом формы (например script:"OBJECTS i=Invoice PROPERTIES (i) date, stock")
  • change - изменяет данные на форме. Параметры:
    • setState - функция запроса изменения состояний.
    • changes - js-объект, содержащий, что именно надо изменить. Структура js-объекта изменения такая же как и у js-объекта состояния, за исключением того, что у js-объекта группы объектов нет / не должно быть поля list, то есть все изменения предполагается делаются для текущего набора объектов. Впрочем, при необходимости, в поле value можно указать значение в виде массива из одного элемента, что будет означать, что текущий объект менять не надо, но изменять значения свойств надо для заданного, а не текущего объекта. Например change(setState, {game:{value:[30], hostGoals:40, guestGoals:30}}), изменит количество голов на 40 и 30, не для текущей игры, а для игры с id объекта 30. Также в js-объекте изменения можно указывать действия (в js-объекте состояния их нет), значения соответствующего поля при этом может быть любым. Например change(setState, {game: {doSmthWithGame : true}})
    • currentState - js-объект текущего состояния. Необязательный параметр. Для обеспечения лучшего UX в асинхронном режиме желательно, чтобы пользователь менял значения именно для тех объектов, которые он видит непосредственно в момент изменения, а не в момент обработки этого изменения (за это время состояние может измениться и текущие объекты станут другими). Соответственно, при вызове этой функции рекомендуется в качестве параметре currentState передать состояние, которое использовалось для отрисовки (render'а) представления, в котором пользователь инициировал данное изменение.
  • close - закрывает форму. Параметры:
    • setState - функция запроса изменения состояний.
  • formCreated - проверяет инициализировалась ли форма (соответственно заполнился ли state). Возвращает boolean. Параметры:
    • state - js-объект состояния
  • numberOfPendingRequests - показывает сколько запросов изменения сейчас в очереди. Возвращает long. Параметры:
    • state - js-объект состояния

В качестве имен групп объектов и свойств используют не имена на форме, а имена экспорта / импорта (которые впрочем если не заданы явно, совпадают с именами на форме). При работе с формой через Form API, действия созданные при помощи операторов работы с объектами NEW и DELETE автоматически получают имена экспорта / импорта NEW и DELETE соответственно (то есть например для добавления объекта, можно вызвать change(setState, {game : {NEW:true}}) ). Также в Form API на форму автоматически добавляется свойство с именем logMessage, в которое записываются все диалоговые сообщения (в том числе возникающие при нарушении ограничений).

к сведению

Аутентификация, stateful и form api поддерживаются только при выполнении http-запросов на веб-сервере. При выполнении http-запросов на сервере приложений (а точнее встроенном в него веб-сервере), заголовки авторизации, также как и параметры с идентификатором сессии, игнорируются (пользователь считается анонимным). Form api на встроенном веб-сервере не поддерживается в принципе.

Примеры​

Action API (Python)​

import json
import requests
from requests_toolbelt.multipart import decoder

lsfCode = ("run(INTEGER no, DATE date, FILE detail) {\n"
" NEW o = FOrder {\n"
" no(o) <- no;\n"
" date(o) <- date;\n"
" LOCAL detailId = INTEGER (INTEGER);\n"
" LOCAL detailQuantity = INTEGER (INTEGER);\n"
" IMPORT JSON FROM detail TO detailId, detailQuantity;\n"
" FOR imported(INTEGER i) DO {\n"
" NEW od = FOrderDetail {\n"
" id(od) <- detailId(i);\n"
" quantity(od) <- detailQuantity(i);\n"
" price(od) <- 5;\n"
" order(od) <- o;\n"
" }\n"
" }\n"
" APPLY;\n"
" EXPORT JSON FROM price = price(FOrderDetail od), id = id(od) WHERE order(od) == o;\n"
" EXPORT FROM orderPrice(o), exportFile();\n"
" }\n"
"}")

order_no = 354
order_date = '10.10.2017'
order_details = [dict(id=1, quantity=10),
dict(id=2, quantity=15),
dict(id=5, quantity=4),
dict(id=10, quantity=18),
dict(id=11, quantity=1),
dict(id=12, quantity=3)]

order_json = json.dumps(order_details)

url = 'http://localhost:7651/eval'
payload = {'script': lsfCode, 'no': str(order_no), 'date': order_date,
'detail': ('order.json', order_json, 'text/json')}

response = requests.post(url, files=payload)
multipart_data = decoder.MultipartDecoder.from_response(response)

sum_part, json_part = multipart_data.parts
sum = int(sum_part.text)
data = json.loads(json_part.text)

##############################################################

print(sum)
for item in data:
print('{0:3}: price {1}'.format(int(item['id']), int(item['price'])))

##############################################################
# 205
# 4: price 5
# 18: price 5
# 3: price 5
# 1: price 5
# 10: price 5
# 15: price 5

Form API (JavaScript)​