Skip to content

Latest commit

 

History

History
1130 lines (760 loc) · 63.9 KB

ECOS_Records.rst

File metadata and controls

1130 lines (760 loc) · 63.9 KB

ECOS Records API

Демо Spring Boot проект Исходный код kotlin/java движка Records API

API, разработанное для организации простого и легко масштабируемого общения между потребителем информации и источником. Источниками данных могут быть БД, alfresco, REST и др.

Преимущества:

  • Единый API для доступа к данным в системе для всех потребителей (Браузер, Мобильное приложение, Система построения отчетов, Индексирование данных, Различные микросервисы и т.д.);
  • Поддержка загрузки данных из связанных сущностей. Например, если у нас договор ссылается на доверенность, то, имея идентификатор договора, мы можем получить любой атрибут связанной доверенности;
  • Оптимальность. Загружаются и вычисляются только те атрибуты, которые нужны потребителю;
  • Простота в разработке – разработчик источника данных описывает все атрибуты, которые могут запросить потребители вне зависимости от сложности их вычисления. Потребитель в запросе указывает только те атрибуты, в которых он заинтересован;
  • Простота поддержки - нет версионирования API т.к. мы в любой момент можем добавлять новые атрибуты, не трогая старые;
  • Тип получаемых данных полностью описывается запросом. Из источника данных мы возвращаем атрибуты с любым типом, а Records API приводит их к нужному для потребителя;
  • Вычисляемые атрибуты. Возможность добавлять атрибуты, которые не хранятся в БД или любом другом хранилище, а вычисляются на основе существующих;
  • Поддержка объединения атрибутов из разных источников. Например, можно написать источник данных, который часть атрибутов будет брать из alfresco, а часть из внешней БД объединяя их по идентификатору.

Note

ECOS Records API - это НЕ GraphQL. Старые версии Records API использовали GraphQL, но начиная с версии 3.0 API полностью самостоятелен.

Общие:

  • Атрибут (Attribute) – свойство или ассоциация сущности;
  • Сущность (Entity) – некоторый объект в системе (договор, доверенность, человек, группа, форма и др.);
  • Запись (Record) – сущность с набором атрибутов и идентификатором записи (RecordRef);
  • Идентификатор записи (RecordRef) – идентификатор источника данных и локальный идентификатор сущности в виде строки.
  • Источник данных (записей) (Records DAO) – источник данных, в котором описана логика базовых CRUD операций для работы с сущностями.

Данные:

  • Граф данных (Data Graph) – представление данных, которые может запросить клиент по схеме атрибутов;
  • Скаляр (Scalar) – некоторое финальное значение в графе данных, у которого нельзя запросить вложенные атрибуты (строка, число и др).
  • Схема атрибутов (Attributes Schema) – описание запроса данных для преобразования графа данных в наборы (map и list) значений скаляров;

Очень часто данные не являются плоским списком, а представляют из себя граф, где сущности ссылаются друг на друга. Ниже показан пример такого среза данных, где у нас есть договор с четыремя атрибутами:

  • Заголовок (Title) - Строка
  • Имя (Name) - Строка
  • Контрагент (Counterparty) - Сложный объект
    • Полное наименование (Full Organization Name) - Строка
    • Адрес контрагента (Counterparty Address) - Строка
  • Валюта (Currency) - Сложный объект
    • атрибуты пропущены для простоты

Data Graph

С точки зрения Records API каждый узел этого графа данных может быть получен как значение одного из скаляров:

  • disp - Человекочитаемый вид значения. (Примеры: Для договора - "Договор №2", Для пользователя - "Иванов Иван");
  • str - Строка;
  • num - Число (Двойная точность. Если точности Double не хватает, то следует использовать str);
  • bool - Булево значение;
  • json - JSON представление значения. Допускаются массивы и объекты;
  • id - глобальный идентификатор значения, который содержит идентификатор источника данных и локальный идентификатор. Актуален для сложных значений вроде "Договор", "Контрагент", "Валюта" и др.;
  • localId - то же что и id, но без идентификатора источника данных;
  • bin - бинарные данные;
  • raw - сырые данные в исходном виде. Это единственный скаляр, при загрузке которого не происходит конвертации исходных данных.

Data Graph with Scalars

Для примера рассмотрим получение полного наименования организации контрагента у договора.

Получение наименования контрагента

Здесь мы используем javascript Records API для загрузки нужного нам атрибута.

Первая строка - получение записи по её идентификатору.

Note

Общий вид идентификатора "приложение/источник_данных@локальный_id", но здесь присутствует только локальный_id. Для API это означает, что приложение = "alfresco", а источник_данных = "" (пустой идентификатор зарезервирован за источником с нодами Alfresco)

Вторая строка - загрузка нужного нам атрибута. Вложенные атрибуты разделены точкой ".", а скаляр определяется знаком вопроса "?"

Note

Допустимая вложенность атрибутов не ограничена

Самый простой способ получить значение атрибута - это указать его имя:

cm:name

Note

Двоеточие - часть имени и не является спец символом в данном контексте.

Если мы не указываем скаляр, то он по умолчанию принимается равным "?disp". То есть запись выше аналогична следующей:

cm:name?disp

Для значений с типом "Строка (String)" разницы между скалярами "?disp" и "?str" нет т.к. вернется одно и то же значение.

Для обращения к вложенному атрибуту следует разделять имена точкой:

counterparty.fullOrgName?str

Если на каком-то из уровней в атрибуте ожидается список значений, то следует использовать квадратные скобки "[]" после имени атрибута:

counterparty[].fullOrgName?str
cm:manager.cm:subordinates[].cm:userName?str
cm:manager.cm:department.managers[].cm:subordinates[].cm:userName?str

Если мы запросили атрибут без указания квадратных скобок, а источник данных вернул список, то мы получим только первый элемент из этого списка или null, если список пустой.

Для получения сразу нескольких атрибутов у вложенного значения можно использовать фигурные скобки:

cm:manager.cm:subordinates[]{userName:"cm:userName?str",firstName:"cm:firstName"}

В результате получим следующую структуру:

[
  {
      "userName": "ivan.ivanov",
      "firstName": "Ivan"
  },
  {
      "userName": "petr.petrov",
      "firstName": "Petr"
  }
]

В атрибутах есть поддержка пост-процессоров, которые позволяют выполнять операции над результатом перед тем как вернуть его клиенту.

Пост-процессоры описываются после атрибута через символ вертикальной черты "|".

Форматирование даты:

cm:created|fmt("yyyy__MM__dd")

Подробнее о шаблоне для форматирования даты можно почитать здесь: https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html

Форматирование числа:

ecos:documentAmount|fmt("00000.00")

Данный формат помогает дополнить число лидирующими нулями, если его целая часть меньше 5 знаков и ограничивает числа после запятой двумя знаками

Подробнее о шаблоне для форматирования чисел можно почитать здесь: https://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html

Значение по умолчанию:

ecos:documentAmount?num|or(0)

Если атрибут ecos:documentAmount вернет null, то вместо него мы получим число 0.

Для процессора "or" есть короткая запись через "!"

ecos:documentAmount?num!0

В процессоре "or" можно использовать другие атрибуты:

cm:title?str!cm:name?str
cm:title?str|or("a:cm:name?str")

В данном примере мы получим значение cm:title или значение cm:name, если cm:title равен null или пустой строке.

Note

Данный атрибут приведен для примера и для получения "заголовок или имя" лучше использовать скаляр "?disp" т.к. у нод alfresco он по умолчанию реализован подобным образом

В полной форме нам нужно указать префикс "a:" чтобы обозначить, что нам нужно значение атрибута, а не константа "cm:name?str" Если нам нужно строковое константное значение в короткой форме, то следует взять значение в кавычки:

cm:title?str!"cm:name"

Добавление префикса или суффикса:

cm:name|presuf("prefix-","-suffix")

Если значение cm:name равно "Имя", то на выходе мы получим "prefix-Имя-suffix" Значение суффикса можно не задавать. Если значение префикса не нужно, а значение суффикса нужно, то первым аргументом можно передать пустую строку.

Процессоры можно объединять:

cm:title!cm:name!"n-a"|presuf("prefix-","-suffix")
  1. Взять заголовок;
  2. Если заголовок пустой, то взять имя;
  3. Если имя пустое, то взять константу "n-a";
  4. Добавить к результату пунктов 1-3 префикс "prefix-";
  5. Добавить к результату пункта 4 суффикс "-suffix".
Список возможных пост-процессоров
Название Аргументы Описание
presuf
prefix: String
suffix: String
Добавить константу в начало и/или в конец строки
or
orValue0: Any
orValue1: Any
orValueN: Any
Вернуть значение по умолчанию если значение атрибута равно null. Если аргумент является строкой
и начинается на "a:", то оставшаяся часть атрибута воспринимается как другой атрибут, который
нужно вычислить и вернуть в результате.
Количество аргументов не ограничено. Аргументы перебираются по очереди
и если он не null (не является null и не вычислился через "a:" в null), то результат сразу возвращается.
rxg
pattern: String
groupIdx: Int = 1
Применить регулярное выражение к результату и вернуть указанную группу.
Примеры:
"some-text" | rxg("some-(.+)") -> text
"some-text-and-more" | rgx("(some)-(text)-(and)-(more)", 2) -> text
join delimiter: String = "," Объединить список значений в строку используя указанный разделитель
hex (3.26.0+)
delimiter: String = ""
Представить base64 строку как HEX строку (список шестнадцатеричных чисел,
где каждый байт представлен двумя символами)
fmt
format: String
locale: String = "en"
timezone: String = "UTC"
Отформатировать число или дату по указанному формату
cast
type: { "str", "num", "bool" }
Преобразует значение в указанный формат.
yaml  

Любую структуру приводит к YAML строке. | Пример:

await Citeck.Records.get('uiserv/form@ECOS_FORM').load('?json|yaml()')

Группировка

В query можно задать атрибуты для группировки через параметр groupBy. Если Records DAO поддерживает группировку (реализует интерфейс RecsGroupQueryDao), то RecordsService ничего не делает с запросом и передает его как есть в DAO. Если же Records DAO не поддерживает группировку, то RecordsService пробует выполнить группировку самостоятельно используя дополнительные запросы. Этот механизм называется "автогруппировка". Так как автогруппировка может быть нежелательна в ряде случаев, то в системе предусмотрен флаг для её отключения:

ecos.webapp.records.queryAutoGroupingEnabled

Если этот флаг выставлен в false и целевой Records DAO не поддерживает группировку, то все запросы с непустым groupBy будут возвращать пустой список.

Если известно. что в каком-то атрибуте лежит строка или MLText структура (объект, где в качестве ключей локаль, а в значении соответствующая строка), то можно применить преобразование "mltext".

Пример:

some.att._as.mltext // получение актуального значения по локали пользователя
some.att._as.mltext.ru // получение значения для конкретной локали
some.att._as.mltext.closest.ru // получение значения для конкретной локали с попыткой вычислить ближайшее не пустое значение
some.att._as.mltext?json // получение значения для всех локалей (если some.att является строкой, то она будет соответствовать локали "en")

Преобразование работает для String, DataValue, MLText, ObjectData, JsonNode (jackson)

При использовании поиска на основе языка предикатов для всех источников записей есть возможность указывать вместо значений динамически вычисляемые атрибуты.

Пример запроса с текущим пользователем:

{
    "t": "eq",
    "att": "actor",
    "val": "${$user.userName}"
}

Если ${} один и занимает всю строку, то "${...}" меняется полностью на вычисленное значение. Таким образом результат вычисления шаблона может быть любым JSON типом включая null. Динамические вставки можно использовать на любом уровне вложенности для любых значений в объектах (можно задавать t, att, val).

Список доступных атрибутов можно посмотреть в разделе "Контекстные атрибуты".

Часто возникают ситуации, когда нужно загрузить атрибуты, которые не относятся напрямую к сущности, а являются контекстными.

Пример таких атрибутов:

  • Текущий пользователь
  • Текущая дата

Для доступа к таким атрибутам при запросе данных к имени атрибута в начале добавляется знак "$".

Т.о. если нам нужно получить имя текущего пользователя, мы можем загрузить следующий атрибут:

$user.cm:userName

Если нам нужно получить текущую дату и отформатировать её:

$now|fmt("yyyy")

Список контекстных атрибутов, которые доступны во всех источниках:

  • user - Текущий пользователь

  • now - Текущая дата

  • auth - Аутентификация текущего пользователя. С помощью этого атрибута можно проверить является ли пользователь частью группы или глобальной роли:

    $auth._has.GROUP_ECOS_ADMINISTRATORS?bool
    $auth._has.ROLE_ADMIN?bool
    
  • str - Атрибут для указания константного строкового значения

  • ref - Атрибут для указания ссылки на другую сущность

  • appName - Имя текущего приложения

  • appInstanceId - Идентификатор инстанса текущего приложения

Если в серверном коде нужно расширить доступный список контекстных атрибутов, то работу с RecordsService нужно выполнять следующим образом:

val contextAtts = mutableMapOf<String, Object>()
contextAtts["customVariable"] = RecordRef.valueOf("emodel/person@admin")

String result = RequestContext.doWithAtts(contextAtts) {
  recordsService.getAtt("any-record", "$customVariable?disp").asText()
}

В качестве значений для контекстных атрибутов могут быть EntityRef'ы (для доступа к другим сущностям) или значения любых других типов.

В [3.45.0] Появилось два новых скаляра - ?raw и ?bin

?raw возвращет данные без преобразования, как есть.

?bin возвращает бинарные данные. При использовании json-формата равнозначен использованию ?str (данные передаются в виде base64 строки), но с использованием форматов, которые поддерживают передачу массивов байт без необходимости трансформации в base64 дает преимущество по размеру передаваемых данных (base64 дает оверхед 33%).

Скаляр ?json удобен для получения всего набора атрибутов из записи.

Синтаксис для js:

_static/records/json_1.png

Синтаксис для java:

_static/records/json_1.png

RecordsService - сервис для работы с абстрактными записями, источником которых может быть любой DAO.

Существует четыре операции, которые можно проделывать над записями:

Методы: query, queryOne

Для поиска записей всегда передается RecordsQuery, который содержит параметры поиска. Помимо самого простого метода для поиска с одним параметром RecordsQuery так же есть варианты с объединенным поиском и запросом атрибутов.

recordsService.queryOne(
  RecordsQuery.create()
        .withLanguage(PredicateService.LANGUAGE_PREDICATE)
        .withQuery(Predicates.and(
                Predicates.eq(ValuePredicateToFtsAlfrescoConstants.TYPE, "cm:person"),
                Predicates.eq("testc:personalNumber", personalNumber)))
        .withConsistency(Consistency.EVENTUAL)
        .addSort(new SortBy("cm:created", true))
        .build());
recordsService.query(RecordsQuery.create()
        .withLanguage(PredicateService.LANGUAGE_PREDICATE)
        .withQuery(Predicates.and(
                Predicates.eq("_type", "emodel/type@testip-inboundPackage"),
                Predicates.eq("testip:isNeedSendToVim", true),
                Predicates.not(
                        Predicates.eq("testip:isAlreadySentToVim", true)
                )
        ))
        .withConsistency(Consistency.EVENTUAL)
        .build());
  • .withLanguage – указываем язык запроса;
  • .withQuery – сам запрос;
  • .withConsistency – Consistency (Согласованность). Возможные варианты: EVENTUAL, TRANSACTIONAL, DEFAULT, TRANSACTIONAL_IF_POSSIBLE
  • .addSort – указываем по какому полю нужна сортировка
  • .build() – сборка запроса

На выходе:

  • при query получаем RecsQueryRes<RecordRef>
  • при queryOne получаем RecordRef

Методы: getAtt, getAtts

recordsService.getAtt(documentRef, "eint:ediProviderType?str").asText();
  • documentRef – record, к которому обращаемся
  • "eint:ediProviderType?str" – параметр, который хотим получить
List<ObjPropertyClass> list = recordsService.getAtt(documentRef, "objProperty[]?json").asList(ObjPropertyClass.class);
RecordAtts recordAtts = recordsService.getAtts(RecordRef.valueOf(nodeRef.toString()),
      Collections.singletonMap("assocId", name + "[]?id"));

Существует два уровня абстрации для получения атрибутов:

DTO Class > Attributes

  • DTO Class - класс, который используется для генерации списка аттрибутов для формирования схемы и запроса атрибутов из DAO.

После получения всех данных из DAO идет создание инстансов переданного DTO класса и наполнение его данными с помощью библиотеки jackson; Список аттрибутов формируется либо из названий полей, либо можно добавить аннотацию AttName для указания атрибута вручную.

  • Attributes - аттрибуты записи в чистом виде. Есть варианты с одним атрибутом, списком атрибутов или набором ключ->значение (Map)

Каждый DAO решает сам создавать или редактировать полученную запись. Если в DAO приходит запись с пустым идентификатором, то это команда к созданию новой записи.

Изменение записи

RecordAtts recordAtts = new RecordAtts();
recordAtts.setId(recordRef);
recordAtts.setAtt("testdl:isOutboundPackageSyncNeeded", false);
recordsService.mutate(recordAtts);

Для обновления записи необходимо указывать .setId() записи которой необходимо изменить

Создание записи

RecordAtts recordAtts = new RecordAtts();
recordAtts.setAtt(AlfNodeRecord.ATTR_TYPE, "testdl:routeTemplate");
recordAtts.setAtt(RecordConstants.ATT_TYPE, "emodel/type@testdl-routeTemplateItem");
recordAtts.setAtt("etype:type","testdl-routeTemplateItem");
recordAtts.setAtt(RecordConstants.ATT_PARENT,
        "/app:company_home/st:sites/cm:ssg-edi/cm:dataLists/cm:testdl-routeTemplate");
recordAtts.setAtt(RecordConstants.ATT_PARENT_ATT, "cm:contains");
recordsService.mutate(recordAtts);

При создании новой записи параметр setId() не указывается.

recordsService.delete(routeTemplate);
  • RecordRef routeTemplate – record который необходимо удалить

RecordRef - это идентификатор записи, который состоит из трех частей:

  1. appName - идентификатор приложения, к которому относится запись;
  2. sourceId - идентификатор локального (для приложения) источника данных, к которому относится запись;
  3. id - локальный идентификатор, который должен быть уникален в пределах источника.

Общий вид: appname/sourceId@id

где / и @ - особые разделители.
  • Если в RecordRef не задан sourceId, то источником по умолчанию считается - "" (пустая строка).

RecordRef является реализацией интерфейса EntityRef

В Alfresco с таким идентификатором зарегистрирован AlfNodesRecordsDAO - источник данных, у которого запись === нода Alfresco. Из этого следует, что NodeRef.toString() === RecordRef.toString() для нод Alfresco;

Уровни детализации от меньшего к большему:

RecordRef.create("emodel", "type", "testdl-counterpartyToAuthority");
  • “emodel” – appName
  • “type” – sourceId

_static/records/records_1.png

Note

Начиная с версии 4.5.0 вместо Citeck.Records.get и Citeck.Records.query можно использовать Records.get и Records.query

Для работы с Records API разработан компонент Citeck.Records, который доступен в глобальном контексте на любой странице системы.

Доступные операции:

  • get(recordRef) - Получить запись по её идентификатору. Ниже представлен список операций с записью;
  • query(query, attributes) - Поиск записей. Первый аргумент - запрос для поиска, а второй - какие атрибуты нам нужны у найденых записей;
  • remove(records) - Удаление записей.

Операции с записью, которая получена через метод "Citeck.Records.get":

  • load(attributes, forceLoad) - Загрузить атрибут или несколько атрибутов. Первым аргументом мы указываем что нужно загрузить, а вторым следует использовать кэш или нет. Второй аргумент опционален и по умолчанию равен false (т.е. кэш активен);
  • att(attributeName, value) - Проставить значение атрибута для записи. Используется перед сохранением записи;
  • save(attsToLoad) - Сохранить изменения в записи, которые были сделаны методом att из предыдущего пункта и загрузить атрибуты, которые передали в attsToLoad (опционально);

Метод save с версии UI 2.8.1 может принимать атрибуты для загрузки. В этом случае на сервер вместе с атрибутами для изменения так же отправляются атрибуты для загрузки в поле attributes тела запроса. Если при вызове save указаны атрибуты для загрузки, то в результате будет тот же формат, что и при вызове метода load.

Структура query:

{
  "sourceId": String // идентификатор источника данных в формате "приложение/id_локального_источника_данных"
  "query": Any // любой формат, который поддерживается источником данных
  "language": String // язык для определения содержимого query. Источник данных может поддерживать несколько языков
  "sortBy": [
      {
          "attribute": String // атрибут для сортировки
          "ascending": Boolean // сортировка должна быть по возрастанию true или по убыванию false
      }
  ],
  "groupBy": [String] // список атрибутов для группировки
  "page": {
      maxItems: Number // максимальное кол-во элементов
      skipCount: Number // количество элементов, которое нужно пропустить при поиске
  }
  "consistency": EVENTUAL | TRANSACTIONAL | DEFAULT | TRANSACTIONAL_IF_POSSIBLE // ожидаемая консистенция данных. EVENTUAL позволяет использовать индексы для поиска элементов
}

Примеры использования:

Запрос ноды:

await Citeck.Records.get("workspace://SpacesStore/16d8668d-7325-49ef-80d3-f2bfdb4c6d00").load({
  'status': 'icase:caseStatusAssoc.cm:title?str',
  'display': '.disp'
});

---
Запрос конфига:

await Citeck.Records.get('ecos-config@ecos-forms-enable').load('.str');

---

await Citeck.Records.query({
  sourceId: 'alfresco/',
  query: 'TYPE:"cm:content"',
  language: 'fts-alfresco',
  page: {     maxItems: 10 }
}, ['cm:title', 'cm:name']);

---
Запрос ФИО пользователя:

var user = Citeck.Records.get('alfresco/people@admin');
await user.load({
  userName: 'cm:userName',
  firstName: 'cm:firstName',
  lastName: 'cm:lastName'
})

---
Запрос ФИО пользователя:

var user = Citeck.Records.get('alfresco/people@admin');
await user.load(['cm:userName', 'cm:firstName', 'cm:lastName'])

---
Запрос имени пользователя:

var user = Citeck.Records.get('alfresco/people@admin');
await user.load('cm:firstName')

---

Пример скрипта для смены статуса:

var doc = Citeck.Records.get('someDocumentRef');
doc.att('_status', 'some_status_id');
doc.save();

---

Получение сразу нескольких атрибутов у вложенного значения:

await Citeck.Records.get('uiserv/rjournal@test587').load(boardRefs[]{id,name}, true)

---

Статус объекта:

await Citeck.Records.get(record).load("_status?str")

---

Проверка enterprise лицензии:

await Citeck.Records.get('emodel/meta@').load('$license.enterprise?bool', true)

Общение с сервером происходит через POST запросы.

Запрос Описание В коде ecos-ui используется

READ_ONLY POST

/gateway/api/records/query
Поиск записей и/или получение атрибутов
Records.query и Records.get("id_сущности").load(атрибуты_для_загрузки)

READ_WRITE POST

/gateway/api/records/delete
Удаление сущностей
Records.remove

READ_WRITE POST

/gateway/api/records/mutate
Создание или изменение сущностей
var rec = Records.get("id_сущности"); rec.att("атрибут", "значение"); rec.save()

В ответе может быть возвращен только тип json.

Возможные коды ответов:

  • 200 OK
  • 401 Unauthorized
  • 500 Internal Server Error

Ошибки отражены в теле ответа по ключу messages и с полем level равным "ERROR".

Пример:

{
  "messages": [
    {
      "level": "ERROR",
      "time": 1653990549261,
      "type": "text",
      "msg": "Some error",
      "requestId": "7848a70e-a449-4b24-abb9-a2a7fbb8ebfa",
      "requestTrace": [
        "gateway:06d039e1766550be603cf98379bbdb22",
        "alfresco:019ca5db-160f-45df-84a6-02750a4f13b7"
      ]
    }
  ],
  "txnActions": [],
  "records": [],
  "hasMore": false,
  "totalCount": 0,
  "version": 1
}

Доступный level только "ERROR".

Для работы с RecordsAPI на kotlin/java бэкенде предусмотрена библиотека ecos-records - https://github.com/Citeck/ecos-records

Подключив библиотеку можно создать RecordsServiceFactory и получить оттуда все сервисы для работы с RecordsAPI. Инициализация сервисов инкапсулирована в RecordsServiceFactory и не требует обязательного наличия DI механизмов.

Основной сервис для работы с RecordsAPI - это ru.citeck.ecos.records3.RecordsService. Пример использования:

Kotlin:

val serviceFactory = RecordsServiceFactory()
val recordsService = serviceFactory.recordsServiceV1

val value = HashMap<String, String>()
value["someKey"] = "someValue"

val attributeValue = recordsService.getAtt(value, "someKey").asText()
println(attributeValue) // someValue

Java:

RecordsServiceFactory serviceFactory = new RecordsServiceFactory();
RecordsService recordsService = serviceFactory.getRecordsServiceV1();

Map<String, String> value = new HashMap<>();
value.put("someKey", "someValue");

String attributeValue = recordsService.getAtt(value, "someKey").asText();
System.out.println(attributeValue); // someValue

Здесь мы создаем новую мапу с одним значением и получаем из неё атрибут с именем someKey через RecordsService.

Есть два основных сценария использования RecordsService:

  • Работа с уже готовыми данными как в примере выше. Нам не нужно никуда отправлять запросы и получение атрибутов проходит в пределах сервиса. В этом режиме доступно только получение атрибутов и Records DAO никак не задействуются.
  • Работа с ссылками (EntityRef). В этом режиме сервис взаимодействует с источниками данных, функционал которых реализован через следующие интерфейсы:
    • RecordsDao базовый интерфейс для всех остальных ниже по списку. Содержит только один метод - String getId(), который используется при регистрации RecordsDao в RecordsService;
    • RecordsQueryDao для поиска записей;
    • RecordsAttsDao (RecordAttsDao) для получения атрибутов по заранее известным идентификаторам записей;
    • RecordMutateDao для создания или редактирования записей;
    • RecordsDeleteDao (RecordDeleteDao) для удаления записей;

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

прим. - Records DAO - это реализация абстрактного понятия "Источник данных". Один Records DAO может представлять разные источники данных.

При работе с Records DAO в зависимости от типа действия происходит следующее:

  • Query. Мы передаем в RecordsQueryDao поисковый запрос и ждем на выходе следующие типы значений (поддерживаются как коллекции этих значений так и значения в одном экземпляре):

    • EntityRef - ссылки на сущности. Если мы получаем ссылки, то сервис обращается к соответствующему RecordsAttsDao для получения атрибутов;
    • String - текстовый результат означает что мы вернули идентификаторы записей, по которым нам нужно получить атрибуты через RecordsAttsDao. Если в строке не указан другой Records DAO, то используется тот же, у которого мы вызывали query;
    • RecsQueryRes - список записей вместе с данными об их общем количестве;
    • Any - любое другое значение, которое обрабатывается с использованием реализаций интерфейса AttValueFactory;
  • Get attributes. Получение атрибутов по идентификаторам записей. Этот метод используется либо с результатом Query из предыдущего пункта либо посредством прямого вызова recordsService.getAtts(...) Метод возвращает любое значение, которое обрабатывается с использованием реализаций интерфейса AttValueFactory;

  • Mutate. Изменение или создание записей через RecordMutateDao

    В Records API создание записи происходит при мутации записи с пустым локальным идентификатором. Т.е. если мы хотим создать сущность в микросервисе emodel в источнике данных types-repo то делаем следующее:

    // здесь следует обратить внимание на строку 'emodel/types-repo@'.
    // Согласно структуре RecordRef'а (ссылка внизу) здесь
    // (AppName - "emodel", SourceId - "types-repo", LocalId - "" (пустая строка))
    let newRecord = Citeck.Records.get('emodel/types-repo@');
    
    newRecord.att("id", "id-value");
    newRecord.att("name", "Custom name");
    // В resultRecord будет созданная запись.
    // Если мы задаем id вручную (как двумя строчками выше),
    // то в resultRecord будет лежать то же самое что мы получим при выполнении
    // Citeck.Records.get('emodel/types-repo@id-value');
    // Если мы не указали вручную ID, то он сгенерируется в виде UUID.
    let resultRecord = await newRecord.save();
    
  • Delete. Удаление записей через RecordsDeleteDao

AttValue - это интерфейс, который представляет собой значение, с которым умеет работать RecordsService при получении атрибутов. Методы интерфейса:

Promise<?> init() // инициализация значения перед тем как начать вычисление атрибутов
Object getId() // идентификатор значения. Может быть как строкой, так и EntityRef
Object getDisplayName() // значение для скаляра "?disp"
String asText() // значение для скаляра "?str"
Object getAs(String type) // значение для спец. атрибута "_as"
Double asDouble() // значение для скаляра "?num"
Boolean asBoolean() // значение для скаляра "?bool"
Object asJson() // значение для скаляра "?json"
Object asRaw() // значение для скаляра "?raw"
Object asBin() // значение для скаляра "?bin"
has(String name) // значение для спец. атрибута "_has"
Object getAtt(String name) // получить значение атрибута по его имени
AttEdge getEdge(String name) // получить мета-информацию об атрибуте по его имени
Object getType() // получить ECOS тип значения

AttValueFactory - это интерфейс для преобразования произвольных типов данных в имплементацию AttValue

// Проинициализировать фабрику. В основном используется для получения конвертеров для других типов.
// Например: attValuesConverter.getFactory(DataValueAttFactory.class)
void init(attValuesConverter: AttValuesConverter)

// Получить реализацию AttValue для значения
AttValue getValue(T value)

// Получить список доступных типов значений, которые может обрабатывать данная фабрика
List<Class<*>> getValueTypes()

// Получить приоритет фабрики. Чем выше приоритет, тем важнее фабрика в случае если для одного и того же типа нашлось две фабрики.
int getPriority()

Для регистрации произвольных AttValueFactory нужно в библиотеке или микросервисе создать следующий файл:

resources/META-INF/services/ru.citeck.ecos.records3.record.atts.value.factory.AttValueFactory

Внутри этого файла должно быть полное имя класса (вместе с пакетом) с вашей реализацией интерфейса AttValueFactory

Пример: https://github.com/Citeck/ecos-records/blob/master/ecos-records/src/test/resources/META-INF/services/ru.citeck.ecos.records3.record.atts.value.factory.AttValueFactory

Если для значения не нашлось подходящего AttValueFactory, то используется стандартная фабрика BeanValueFactory. Эта фабрика работает со значением как с бином, у которого ищутся геттеры для атрибутов.

Например, если у нас есть следующий бин:

static class TestDto {
  private String field;
  void setField(String value) {
    this.field = value;
  }
  String getField() {
    return field;
  }
}

То с точки зрения BeanValueFactory у этого бина есть значение с одним атрибутом "field". Пример работы:

RecordsServiceFactory serviceFactory = new RecordsServiceFactory();
RecordsService recordsService = serviceFactory.getRecordsServiceV1();

TestDto value = new TestDto();
value.setField("field-value");

String attributeValue = recordsService.getAtt(value, "someKey").asText();
System.out.println(attributeValue); // field-value

Если же мы хотим изменить имя атрибута не меняя названия методов, то можно воспользоваться аннотацией AttName:

static class TestDto {
  private String field;
  void setField(String value) {
    this.field = value;
  }
  @AttName("otherName")
  String getField() {
    return field;
  }
}
...
TestDto value = new TestDto();
value.setField("field-value-2");

String attributeValue = recordsService.getAtt(value, "otherName").asText();
System.out.println(attributeValue); // field-value-2

Аннотация @AttName помогает задать произвольное имя атрибута. Её можно использовать:

  • На геттере, чтобы дать произвольное название атрибуту;
  • На сеттере для конвертации DTO -> Схема атрибутов для запроса; (см. методы recordsService.getAtts(Any record, Class<?> atts))
  • Аннотация на поле работает как для сеттера так и для геттера если они есть;

Аннотация @AttName может в качестве аргумента принимать значение "...". Такая запись означает, что все атрибуты из поля с этой аннотацией будут доступны так же и в нашем значении. Пример:

static class ParentDto {
  @AttName("...")
  private ChildDto child = new ChildDto(); // опустим сеттер, чтобы не усложнять пример
  public ChildDto getChild() {
    return child;
  }
}
static class ChildDto {
   public String getValue(): String {
     return "abc"; // геттер не обязательно должен отдавать значение поля. Его поведение может быть произвольным
   }
}
...
ParentDto value = new ParentDto();

// Если бы аннотация AttName отсутствовала, то до значения 'abc' мы бы могли добраться так:
// recordsService.getAtt(value, "child.value").asText();
// Но с аннотацией @AttValue("...") можно обращаться к вложенному атрибуту так:

String attributeValue = recordsService.getAtt(value, "value").asText();
System.out.println(attributeValue); // abc

Так же особое значение имеют аннотации AttName где в качестве аргумента указан один из скаляров с вопросительным знаком. Например: @AttName("?str"). Такие геттеры вызываются при загрузке скаляров.

BeanValueFactory так же ищет в бине ряд специальных методов по их имени и аргументам (тип возвращаемого значения не важен):

Object getId() // значение для скаляра ?id
Object getAsStr() // значение для скаляра "?str"
Object getAsNum() // значение для скаляра "?num"
Object getAsBool() // значение для скаляра "?bool"
Object getAsJson() // значение для скаляра "?json"
Object getAsRaw() // значение для скаляра "?raw"
Object getAsBin() // значение для скаляра "?bin"
Object getEcosType() // значение для атрибута "_type"
Object getAs(String name) // значение для спец. атрибута "_as"
Object has(String name) // значение для спец. атрибута "_has"
Object getEdge(String name) // значение для спец. атрибута "_edge"
Object getAtt(String name) // Значение атрибута по имени если не получилось найти геттер для него

Для отображаемого имени нашего бина BeanValueFactory ищет следующие методы в порядке убывания приоритета (используется первый найденный):

Object getDisplayName()
Object getLabel()
Object getTitle()
Object getName()
  • Контекст - область, которая выделена с помощью скобок или кавычек {}, [], (), "", '' или не выделена ничем (корневая область или корневой контекст);
  • Алиас - псевдоним для атрибута. Пример: в конструкции someAlias:name someAlias является алиасом и возможный результат вычисления - someAlias:"Договор №2";
  • Экранирование символа - добавление перед символом знака \. Необходимо в тех случаях, когда спец-символ должен быть обработан как обычный символ;
  • Спец-символ - символ, который в определенном контексте имеет специальное значение.
  • Скаляр - конечный атрибут, который не может содержать вложенных атрибутов. Может быть одним из ?id, ?str, ?disp, ?num, ?assoc, ?localId, ?bool, ?json.

Note

  1. Экранирование спец-символов необходимо только в текущем контексте и не требуется во вложенных контекстах.

Общий вид атрибута:

path0[].path1{INNER}|proc0(arg0,arg1)|proc1(arg0,arg1) (1)

path0[].path1 - это путь из атрибутов. Элементы пути объединяются через точку. Если точка является частью имени атрибута, то её следует экранировать.

Все атрибуты в пути кроме последнего имеют ровно один внутренний атрибут без пост-процессоров и алиаса. Последний атрибут в пути может иметь любое количество вложенных атрибутов, но не имеет алиаса. Все атрибуты в пути кроме первого не имеют пост-процессоров. Первый атрибут в пути может иметь любое количество пост-процессоров, которые указываются в конце после {INNER}. Любой элемент пути из атрибутов может иметь окончание [], которое при наличии означает, что атрибут множественный.

{INNER} содержит вложенные атрибуты с алиасами, которые разделены через запятую. Алиас не обязателен. Если он отсутствует, то для результата используется первое имя в пути атрибутов.

Пример значения {INNER}:

{alias0:attribute0,alias1:attribute1,attribute2}

В aliasN спец-символами являются , и :. Вместо attributeN допускается синтаксис (1), но c экранированием запятых , и если отсутствует алиас, то следует экранировать : (см. Примечание 1). Если алиас равен первому элементу в пути атрибутов, то это равнозначно отсутствию алиаса.

Вместо {INNER} при наличии только одного вложенного атрибута без алиаса и процессоров допускается запись без фигурных скобок. В таком случае если вложенный атрибут не является скаляром, то перед ним добавляется точка. Перед скаляром ничего не добавляется т.к. он уже содержит разделительный символ ?.

Примеры:

name?str == name{?str}
name.title?str == name{title{?str}}

Если атрибут заканчивается на скаляр ?disp (att0?disp или att0{?disp}), то допускается опустить окончание ?disp в атрибуте т.к. это скаляр по-умолчанию.

Пример:

name?disp == name

При описании атрибута допускается использование пост-процессоров, которые вызываются с результатом вычисления атрибута:

proc0(arg0,arg1)
  • procN - имя пост-процессора;
  • argN - аргументы, которые отделяются друг от друга запятыми. Допускаются значения аналогичные формату json - https://www.json.org/json-en.html , но с возможностью использовать для строк одинарные кавычки вместо двойных;

Пост-процессоры объединяются через символ | и выполняются слева направо аналогично unix pipeline. Пост-процессоры могут быть частью любого атрибута на любом уровне вложенности.

Для пост-процессора с типом "or" доступен дополнительный синтаксис с использованием !. Возможные варианты значения после !:

  1. Значение в двойных или одинарных кавычках означает константную строку; (some!'constant' == some|or('constant'))
  2. При отсутствии значения парсер подбирает нужный аргумент в зависимости от скаляра перед знаком !:
  • ?bool! -> false
  • ?json! -> {};
  • ?num! -> 0;
  • иначе -> "".
  1. null означает пустое значение; (some!null == some|or(null))
  2. true или false - булево значение; (some!true == some|or(true))
  3. Если первый символ число - числовое значение; (some!123 == some|or(123))
  4. Если ни один из вышестоящих вариантов не подошел, то считается, что указано имя атрибута, который нужно вернуть в случае если результат вычисления атрибута до ! оказался null; (some!other == some|or('a:other'))

Между частями атрибута (алиас, путь, вложенные атрибуты, пост-процессоры, аргументы) допускается использование любого количества пробельных символов (\n, \t, \r, ).

Модель атрибута:

SchemaAtt {
    alias: String,
    name: String,
    multiple: Boolean,
    inner: List<SchemaAtt>,
    processors: List<AttProcDef>
}

Модель пост-процессора:

AttProcDef {
    type: String,
    arguments: List<DataValue>
}