Обращение из внешней системы
Вызов действий (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