Обращение из внешней системы
Вызов действий (Action API)
Платформа предоставляет возможность внешним системам обращаться к разработанной на lsFusion системе с использованием различных сетевых протоколов. Интерфейсом такого взаимодействия является вызов некоторого действия с заданными параметрами и, при необходимости, возврат значений некоторых свойств (без параметров) в качестве результатов. Предполагаются, что все объекты параметров и результатов являются объектами встроенных классов.
Задание действия
Вызываемое действие может задаваться одним из трех способов:
EXEC- задается имя вызываемого действия.EVAL- задается код на языке lsFusion. Предполагается, что в этом коде присутствует объявление действия с именемrun, именно это действие и будет вызвано.EVAL ACTION- задается код действия на языке lsFusion. Для обращения к параметрам можно использовать спецсимвол$и номер параметра (начиная с1).
Протоколы
На данный момент в платформе поддерживаются следующие сетевые протоколы:
HTTP
Взаимодействие по этому протоколу поддерживается как с сервером приложений (порт 7651), так и с веб-сервером, если он установлен (подробнее см. Серверы).
Формат URL, в зависимости от способа задания действия, выглядит следующим образом:
EXEC-http://адрес сервера:порт/exec?action=<имя действия>илиhttp://адрес сервера:порт/exec/<имя действия>. Должен быть задан либо параметрaction, либо имя действия в пути URL.EVAL-http://адрес сервера:порт/eval?script=<код>. Если параметрscriptне задан, то предполагается, что код передается первым параметром BODY.EVAL ACTION-http://адрес сервера:порт/eval/action?script=<код действия>. Если параметрscriptне задан, то предполагается, что код передается первым параметром BODY.
Для EXEC имя действия - это либо составное имя действия, либо его EXTID (составное имя пробуется первым). В варианте с путем платформа ищет действие жадно: сначала полный путь пробуется как имя действия (с заменой любых / на _ при поиске по составному имени), и если такого действия нет - последний сегмент пути отбрасывается, и поиск повторяется, пока не будет найдено совпадение или не будет возвращено 404. Оставшаяся после имени найденного действия часть пути передается в свойство System.actionPathInfo[]. Например, для /exec/df/fdf/dffd платформа пробует действия df_fdf_dffd, затем df_fdf, затем df; если совпало только df, в System.actionPathInfo[] записывается fdf/dffd.
Серверы
HTTP API обслуживается двумя типами серверов:
- Сервер приложений - легковесный HTTP-сервер, встроенный в сервер приложений, слушает по умолчанию порт
7651. Обслуживает Action API по/exec//eval//eval/action, с аутентификацией через заголовкиAuthorization: Basic/Authorization: Bearerи stateful-сессиями через параметрsession. Интерактивные действия здесь принимаются только через заголовокNeed-Notification-Id. - Веб-сервер - отдельное веб-приложение (разворачиваемое в сервлет-контейнере, например Tomcat), слушает на своём порте. Обслуживает веб-клиент lsFusion (браузерный пользовательский интерфейс) и на том же порте - HTTP API: тот же Action API, что и сервер приложений, плюс Form API по
/formи вариант с редиректом в браузере для интерактивных действий.
Запрос адресуется как http://<адрес сервера>:<порт><эндпойнт> для соответствующего сервера.
Параметры
Параметры могут передаваться как в строке запроса (добавлением в ее конец строк формата &p=<значение параметра>), так и в теле запроса (BODY). При этом предполагается, что для выполняемого действия, сначала подставляются параметры URL (в порядке их следования в запросе), а только потом параметры BODY.
Если у вызываемого действия интерфейсные параметры именованы (например, run(INTEGER no, DATE date)), параметр запроса, имя которого совпадает с именем интерфейсного параметра, связывается с ним по имени. Описанная выше позиционная подстановка затем заполняет оставшиеся слоты только из неиспользованных параметров p= (URL или application/x-www-form-urlencoded тело) и не-urlencoded параметров BODY (частей multipart или одиночного тела); прочие именованные URL/urlencoded параметры, не совпавшие с интерфейсным именем, игнорируются.
При обработке параметров BODY, параметры с типом контента из следующей таблицы считаются файлами, и передаются в параметры действия в виде объектов файлового класса (FILE, PDFFILE и т.п.). При этом в расширение файла записывается соответствующее расширение из упомянутой таблицы. Если тип контента отсутствует в этой таблице, но начинается на application, то параметр все равно считается файлом, а в расширение этого файла записывается правая часть типа контента (например для типа application/abc в расширение файла записывается abc). Параметры с типом контента application/null считаются равными NULL.
Такой параметр BODY при привязке к интерфейсному параметру действия, класс которого не является файловым, разбирается как строка и автоматически преобразуется к классу этого параметра; пустые строки при этом становятся NULL. Преобразование применяется только к этой привязке - в свойствах запроса ниже тот же параметр всегда хранится как файловое значение.
Свойства запроса
Заголовки выполняемого запроса автоматически сохраняются в свойство System.headers[TEXT]. Так, в единственный параметр этого свойства записывается название заголовка, а в значение свойства - значение этого заголовка.
Cookie выполняемого запроса по аналогии сохраняются в свойство System.cookies[TEXT]: в единственный параметр записывается имя cookie, а в значение свойства - значение этого cookie.
К отдельным параметрам запроса можно также обращаться по имени. Для URL-параметров имя берётся из строки запроса (&<имя>=<значение>); для тела application/x-www-form-urlencoded - из ключа каждой пары; для частей multipart/form-data - из поля name заголовка Content-Disposition каждой части. Для любого другого тела берётся единое имя из поля name заголовка Content-Disposition самого запроса (если заголовка нет - используется имя body); части не-form-data multipart/* тела наследуют это имя - их собственный Content-Disposition не читается - за исключением частей с типом application/x-www-form-urlencoded, ключи которых становятся именами параметров по правилу выше.
Разделение между System.params[TEXT, INTEGER] и System.fileParams[TEXT, INTEGER] определяется по типу контента в момент разбора запроса и применяется рекурсивно к частям multipart/* тела. URL-параметры и любое тело или часть с типом application/x-www-form-urlencoded читаются как TEXT из свойства System.params[TEXT, INTEGER]. Любое другое тело или часть (включая обычные текстовые поля multipart/form-data, у которых нет собственного типа контента) читается как NAMEDFILE (с сохранением исходного имени файла, если оно было) из свойства System.fileParams[TEXT, INTEGER]. Часть без типа контента и без каких-либо байт тела полностью пропускается. Первый параметр обоих свойств - имя параметра, второй - индекс (начиная с 0) среди параметров с одним именем (например, несколько p= параметров или одноимённых частей тела доступны под одним именем с индексами 0, 1 и т.д.); сокращенные формы System.params[TEXT] и System.fileParams[TEXT] возвращают первое вхождение.
Также платформа заполняет следующие свойства информацией о текущем запросе:
System.method[]- HTTP-метод запроса.System.contentType[]- тип контента тела запроса.System.body[]- необработанное тело запроса (на веб-сервере телоapplication/x-www-form-urlencodedможет быть пересобрано из разобранной карты параметров, если сервлет уже прочитал поток - в этом случае точные байты / порядок / кодирование не гарантированно совпадают с исходными).System.query[]- необработанная строка запроса.System.scheme[],System.webHost[],System.webPort[],System.contextPath[],System.servletPath[],System.pathInfo[]- составные части URL запроса. Вспомогательные свойстваSystem.origin[](схема, хост и порт),System.webPath[](origin плюс context path) иSystem.url[](от схемы до пути, без query string) возвращают соответствующие склеенные URL.
Результаты
Свойства, значения которых необходимо вернуть в качестве результата, передаются в строке запроса, добавлением в ее конец строк формата &return=<имя свойства>. При этом предполагается, что значения указанных свойств возвращаются в порядке их следования в строке запроса. По умолчанию, если ни одно свойство результата не задано, результирующим свойством считается первое свойство с не NULL значением из следующего списка.
Если результат запроса является файлом (FILE, PDFFILE и т.п.), то тип контента ответа, в зависимости от расширения файла, определяется в соответствии со следующей таблицей. Если расширение файла отсутствует в этой таблице, тип контента устанавливается равным application/<расширение файла>.
Расширение файла при этом определяется автоматически по аналогии с оператором WRITE.
Во всех трех верхних случаях, если значение результата равняется NULL, то вместо расширения файла в тип контента подставляется строка null (например application/null), а в качестве самого ответа возвращается пустая строка.
Результаты запроса, отличные от файловых, преобразуются к строкам и передаются с типом контента text/plain. NULL значения возвращаются как пустые строки.
Значения свойства System.headersTo[TEXT] автоматически записываются в заголовки результата запроса. Так, из единственного параметра этого свойства читается название заголовка, а из значения свойства - значение этого заголовка.
Значения свойства System.cookiesTo[TEXT] аналогично записываются в cookie ответа (один заголовок Set-Cookie на каждый cookie). Из единственного параметра этого свойства читается имя cookie, а из значения свойства - значение этого cookie.
HTTP-код ответа читается из свойства System.statusHttpTo[] (по умолчанию 200).
Несколько результатов / параметров в 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).
На веб-сервере контейнер, который индексирует сессии по параметру session, живёт внутри HTTP-сессии - поэтому stateful-вызовы должны выполняться в рамках одной и той же HTTP-сессии, обычно поддерживаемой через session cookie, которую браузеры и HTTP-библиотеки передают автоматически. На сервере приложений контейнер общий для всего процесса сервера, и cookie не требуется.
В текущей реализации платформы при использовании сессий элементы системы (например локальные свойства) созданные в текущем вызове удаляются, то есть в последующих вызовах не видны.
Аутентификация
При выполнении http-запроса часто бывает необходимо идентифицировать пользователя, от чьего имени будет выполняться заданное действие. На данный момент в платформе поддерживается два типа аутентификации:
- Базовая аутентификация - в заголовке
Authorization: Basic <credentials>в кодированном виде передаются пользователь и пароль. - Аутентификация на базе токена - состоит из двух этапов:
- На первом этапе необходимо выполнить действие
Authentication.getAuthToken[]c базовой аутентификацией. Результатом этого действия будет токен аутентификации с фиксированным временем действия (по умолчанию один день). Пример запроса:http://localhost/exec?action=getAuthToken. - Далее полученный токен можно использовать для аутентификации в течении периода его действия, передавая его в заголовке
Authorization: Bearer <token>(по аналогии с JWT, который и используется в текущей реализации платформы для генерации токенов аутентификации)
- На первом этапе необходимо выполнить действие
Возможность выполнения запроса также зависит от настройки enableAPI, которая позволяет полностью отключить API, ограничить его авторизованными запросами или дополнительно разрешить анонимные запросы. Отдельное действие можно пометить @@noauth, чтобы обойти и проверку аутентификации, и enableAPI, или @@api, чтобы разрешить его при enableAPI=0 (всё ещё требуя авторизованного пользователя).
Интерактивные действия
Через API может потребоваться вызвать действие, которое взаимодействует с пользователем - открывает форму, показывает сообщение, запрашивает ввод и т.п. Поскольку в самом HTTP-ответе нет UI, платформа перенаправляет такое действие в уже запущенный lsFusion-клиент пользователя: сервер создаёт уведомление (notification), а клиент его подхватывает и исполняет действие у себя.
Когда действие считается интерактивным:
@@ui- всегда интерактивно.@@noui- никогда не определяется как интерактивное, и действие выполняется синхронно (если только клиент явно не запросил идентификатор уведомления через заголовокNeed-Notification-Id- он обрабатывается всегда).- Без аннотаций - интерактивно автоматически, если запрос пришёл с навигацией в браузере (сигнал - заголовок
sec-fetch-mode: navigate) и тело действия использует какую-либо интерактивную возможность; иначе синхронно.
Как клиент получает уведомление:
- Редирект в браузере (по умолчанию) - HTTP
302на/push-notification?notification_id=<идентификатор уведомления>. Переход по этому редиректу в браузере доставляет уведомление в уже открытую там lsFusion-вкладку через её service worker. - Идентификатор уведомления (при заголовке
Need-Notification-Idв запросе) - HTTP200с идентификатором уведомления (в видеINTEGER) в теле ответа. Рассчитан на не-браузерных клиентов, которым нужно доставить этот идентификатор в запущенный lsFusion-клиент через какой-то другой канал.
Интерактивные действия дополнительно требуют, чтобы настройка enableUI разрешала вызов, в дополнение к обычной проверке enableAPI.
Ошибки
В случае ошибки ответ содержит соответствующий HTTP-код:
404- действие, заданное через?action=или в пути URL, не найдено.401- необходима аутентификация или она не прошла. На веб-сервере анонимный интерактивный запрос вместо этого перенаправляется на/login.500- любое другое необработанное исключение при обработке запроса, в том числе когда API отключен настройкойenableAPI.
Для 404, 500 и других статусов от серверного исключения тело ответа - text/html с сообщением об ошибке; для большинства исключений добавляются Java- и lsFusion-стеки вызовов, за исключением RemoteMessageException (пользовательское сообщение платформы), который возвращает только сообщение. Тело 401 содержит только короткое сообщение об ошибке. На веб-сервере редирект на /login идёт без тела - исключение сохраняется в HTTP-сессии, а исходный запрос кэшируется для повтора после логина.
Работа с формами (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-объекта массиваlistjs-объекта группы отображения этого свойства. - Представление свойства равно
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, в которое записываются все диалоговые сообщения (в том числе возникающие при нарушении ограничений).
Примеры
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