В этой статье я хотел бы представить проект RESTForms — универсальный REST API бэкэнд на InterSystems Cache 2016.1+ для современных веб-приложений. Идея проекта проста — после написания нескольких REST API стало понятно, что, как правило, REST API состоит из двух частей:
- Работа с хранимыми данными
- Пользовательская бизнес-логика
И, хотя вам придется писать свою собственную бизнес-логику, RESTForms предоставляет все необходимое для работы с хранимыми данными из коробки.
Варианты использования:
- У Вас уже есть модель данных в Cache, и Вы хотите представить некоторую (или всю) хранящуюся информацию в форме REST API.
- Вы разрабатываете новое Cache приложение и хотите сразу предоставлять REST API.
Клиент
Этот проект разработан как бэкэнд для JS веб-приложений, поэтому JS сразу может начинать работу с RESTForms, не требуется преобразования форматов данных и т.п.
Возможности
Что уже можно делать с RESTForms:
- CRUD по классу данных — возможно получить метаданные класса, создавать, обновлять и удалять свойства класса.
- CRUD по объекту — возможно получать, создавать, обновлять и удалять объекты.
- R по наборам (через SQL) — защита от SQL-инъекций.
- Selfdiscovery – сначала вы получаете список доступных классов, после этого вы можете получить метаданные класса, и на основе этих метаданных выполнять CRUD запросы по объекту.
Пути
Далее представлена таблица доступных методов API, которая демонстрирует то, что Вы можете сделать через RESTForms.
| URL | Описание |
|---|---|
| test | Тестовый метод |
| form/info | Список классов |
| form/info/all | Метаинформация всех классов |
| form/info/:class | Метаинформация одного класса |
| form/field/:class | Добавить свойство в класс |
| form/field/:class | Изменить свойство |
| form/field/:class/:property | Удалить свойство |
| form/object/:class/:id | Получить объект |
| form/object/:class/:id/:property | Получить свойство объекта |
| form/object/:class | Создать объект |
| form/object/:class/:id | Обновить объект из динамического объекта |
| form/object/:class | Обновить объект из объекта класса |
| form/object/:class/:id | Удалить объект |
| form/objects/:class/:query | Выполнить SQL запрос |
| form/objects/:class/custom/:query | Выполнить пользовательский SQL запрос |
Установка
- Загрузите и импортируйте из последнего релиза на странице релизов 20161.xml (для Cache 2016.1) или 201162.xml (для Cache 2016.2 +) в любую область
- Создайте новое веб-приложение
/formsс классом брокеромForm.REST.Main - Откройте http://localhost:57772/forms/test?Debug в браузере, чтобы проверить установку (должен выводиться
{"Status": "OK"}, возможно, будет запрошен пароль). - Если Вы хотите сгенерировать тестовые данные, вызовите:
do ##class(Form.Util.Init).populateTestForms()
Как начать использовать RESTForms?
- Импортируйте проект из GitHub (рекомендуется: добавить его как подмодуль (submodule) в ваш собственный репозиторий или просто загрузить релиз).
- Для каждого класса, который Вы хотите представить через RESTForms:
- Унаследуйте от класса адаптера (Form.Adaptor)
- Определите свойство, используемое в качестве "имени" объекта
- Определите свойства, которые надо отображать
- Используйте RESTForms API для работы с хранимым классом. Как пример сразу можно получить UI для ваших хранимых классов — RESTForms UI. Ниже пример того, как RESTForms UI отображает тестовый класс Person
Cписок объектов класса:
И объект класса:

Пример использования
Во-первых, вы хотите знать, какие классы доступны. Чтобы получить эту информацию, вызовите:
http://localhost:57772/forms/form/infoВы получите в ответ что-то вроде этого:
[
{ "name":"Company", "class":"Form.Test.Company" },
{ "name":"Person", "class":"Form.Test.Person" },
{ "name":"Simple form", "class":"Form.Test.Simple" }
]На данный момент с RESTForms поставляются 3 тестовых класса. Давайте посмотрим метаданные для формы Person (класс Form.Test.Person). Чтобы получить эти данные, нужно вызвать:
http://localhost:57772/forms/form/info/Form.Test.Person{
"name":"Person",
"class":"Form.Test.Person",
"displayProperty":"name",
"objpermissions":"CRUD",
"fields":[
{
"name":"name",
"type":"%Library.String",
"collection":"",
"displayName":"Name",
"required":0,
"category":"datatype"
},
{
"name":"dob",
"type":"%Library.Date",
"collection":"",
"displayName":"Date of Birth",
"required":0,
"category":"datatype"
},
{
"name":"ts",
"type":"%Library.TimeStamp",
"collection":"",
"displayName":"Timestamp",
"required":0,
"category":"datatype"
},
{
"name":"num",
"type":"%Library.Numeric",
"collection":"",
"displayName":"Number",
"required":0,
"category":"datatype"
},
{
"name":"аge",
"type":"%Library.Integer",
"collection":"",
"displayName":"Age",
"required":0,
"category":"datatype"
},
{
"name":"relative",
"type":"Form.Test.Person",
"collection":"",
"displayName":"Relative",
"required":0,
"category":"form"
},
{
"name":"Home",
"type":"Form.Test.Address",
"collection":"",
"displayName":"House",
"required":0,
"category":"serial"
},
{
"name":"company",
"type":"Form.Test.Company",
"collection":"",
"displayName":"Company",
"required":0,
"category":"form"
}
]
}Что все это значит?
Метаданные класса:
- name – отображаемое имя класса
- class – название хранимого класса
- displayProperty – свойство объекта, использующееся при отображении объекта
- objpermissions – что может делать пользователь с объектом. В нашем случае пользователь может читать (Read), создавать (Create) новые объекты, изменять (Update) и удалять (Delete) существующие объекты.
Метаданные свойств:
- name – название свойства
- collection – является ли класс коллекцией (списком или массивом)
- displayName – отображаемое имя свойства
- required – обязательное ли свойство
- type – тип свойства
- category – категория типа свойства. Это обычные категории класса Cache, кроме всех классов, наследующихся от адаптера RESTForms — они имеют категорию "form"
/// Test form: Person
Class Form.Test.Person Extends (%Persistent, Form.Adaptor)
{
/// Отображаемое имя формы
Parameter FORMNAME = "Person";
/// Разрешения
/// Объекты этого класса могут быть Созданы (C), Получены (R), Изменены (U), и удалены (D)
Parameter OBJPERMISSIONS As %String = "CRUD";
/// Свойство "имени" объекта
Parameter DISPLAYPROPERTY As %String = "name";
/// Свойство сортировки по-умолчанию
Parameter FORMORDERBY As %String = "dob";
/// Имя
Property name As %String(DISPLAYNAME = "Name");
/// Дата рождения
Property dob As %Date(DISPLAYNAME = "Date of Birth");
/// Число
Property num As %Numeric(DISPLAYNAME = "Number") [ InitialExpression = "2.15" ];
/// Возраст, вычисляется автоматически
Property аge As %Integer(DISPLAYNAME = "Age") [ Calculated, SqlComputeCode = { set {*}=##class(Form.Test.Person).currentAge({dob})}, SqlComputed, SqlComputeOnChange = dob ];
/// Вычисление возраста
ClassMethod currentAge(date As %Date = "") As %Integer [ CodeMode = expression ]
{
$Select(date="":"",1:($ZD($H,8)-$ZD(date,8)\10000))
}
/// Родственник - ссылка на другой объект класса Form.Test.Person
Property relative As Form.Test.Person(DISPLAYNAME = "Relative");
/// Адрес
Property Home As Form.Test.Address(DISPLAYNAME = "House");
/// Ссылка на компания, в которой человек работает
/// http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_relationships
Relationship company As Form.Test.Company(DISPLAYNAME = "Company") [ Cardinality = one, Inverse = employees ];
}Добавляем класс в RESTForms
Чтобы сделать хранимый класс доступным для RESTForms, надо:
- Добавить наследование от Form.Adaptor
- Добавить параметр
FORMNAMEсо значением – имя класса - Добавить параметр
OBJPERMISSIONS– что можно делать с объектами класса (CRUD) - Добавить параметр
DISPLAYPROPERTY– имя свойства, используемое для отображения имени объекта - Добавить параметр
FORMORDERBY– свойство по умолчанию для сортировки по запросам с использованием RESTForms - Для каждого свойства, которое хочется видеть в метаданных, надо добавить параметр свойства DISPLAYNAME
После того как мы сгенерировали некоторые тестовые данные (см. Установку, шаг 4), давайте получим Person с идентификатором 1. Чтобы получить объект, вызовем:
http://localhost:57772/forms/form/object/Form.Test.Person/1И получим ответ:
{
"_class":"Form.Test.Person",
"_id":1,
"name":"Klingman,Rhonda H.",
"dob":"1996-10-18",
"ts":"2016-09-20T10:51:31.375Z",
"num":2.15,
"аge":20,
"relative":null,
"Home":{
"_class":"Form.Test.Address",
"House":430,
"Street":"5337 Second Place",
"City":"Jackson"
},
"company":{
"_class":"Form.Test.Company",
"_id":60,
"name":"XenaSys.com",
"employees":[
null
]
}
}Чтобы изменить объект (в частности, свойство num), вызовем:
PUT http://localhost:57772/forms/form/object/Form.Test.PersonС телом:
{
"_class":"Form.Test.Person",
"_id":1,
"num":3.15
}Обратите внимание на то, что для улучшения скорости, только _class, _id и измененные свойства должны быть в теле запроса.
Теперь, давайте создадим новый объект. Вызовем:
POST http://localhost:57772/forms/form/object/Form.Test.PersonС телом:
{
"_class":"Form.Test.Person",
"name":"Test person",
"dob":"2000-01-18",
"ts":"2016-09-20T10:51:31.375Z",
"num":2.15,
"company":{ "_class":"Form.Test.Company", "_id":1 }
}Если создание объекта завершилось успешно, RESTForms вернёт идентификатор:
{"Id": "101"}В противном случае, будет возвращена ошибка в формате JSON. Обратите внимание на то, что на персистентные свойства необходимо ссылаться через свойства _class и _id.
Демо
Вы можете попробовать RESTForms онлайн здесь (пользователь: Demo, пароль: Demo). Кроме того, есть приложение RESTFormsUI — редактор для данных RESTForms. Демо стенд доступен здесь (пользователь: Demo, пароль: Demo). Скриншот списка классов:

Заключение
RESTForms выполняет почти всю работу, требуемую от REST API в отношении хранимых классов.
Что же дальше?
В этой статье я только начал говорить о функциональных возможностях RESTForms. В следующей я бы хотел рассказать о некоторых дополнительных функциях, а именно о запросах, которые позволяют клиенту безопасно получать наборы данных без риска SQL-инъекций.
ZitRo
Полезная штука. Можно поинтересоваться,
1. Есть ли возможность сделать какие-либо ограничивающие правила, кроме
OBJPERMISSIONS, которые бы не позволили клиентам сделать «слишком много» на сервере? Например, какой-то декларативный механизм, чтобы каждый клиент мог редактировать только свойPackage.Test.Client. Пока что выглядит так, что для такой цели нужно писать свой REST endpoint. Или это уже beyond the scope of RESTForms?2. При успешном создании объекта RESTForms возвращает
{"Id": "101"}. Всё время спрашиваю себя, почему не{"_id": "101"}?Спасибо!
eduard93
checkObjPermissionStatusдляPackage.Test.Clientкласса — он определяет предоставлять доступ или нет и по умолчанию проверяет наличие C/R/U/D но может быть переопределён для проведения более сложных проверокOnPreDispatchдля контроля доступа