Перейти к основному содержимому
Версия: 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)