В данном руководстве мы рассмотрим работу разработчика с платформой ”E.Soft Process Automation Platform (E.PAP)”.
Подробно остановимся на описании механизмов платформы, позволяющих разработчику описать предметную область, задать маршруты движения сущностей, обрабатывать события системы.
Объекты в хранилище данных будем описывать при помощи метаданных следующим образом. В системе принято за правило описывать каждую сущность в отдельном файле .xml.
Для создания нового объекта необходимо вызвать метод создания нового объекта Create new metadata.
Блок <meta> состоит из тега <meta> с атрибутом typeId=”entity-name”, где значение атрибута соответствует имени сущности.
NB: в целом для описания любой сущности в системе используются следующие параметры, определяющие уникальную сущность:
Описание полей сущности содержится внутри тега <fields>.
Поля могут быть одного из следующих типов:
Остановимся на некоторых типах более подробно.
Обычно определяется в начале каждой сущности. Для данного поля доступен для назначения набор атрибутов alias, таких как “_table”, “_typeId”, “_id”, “_deleted” и др. Атрибутом, обязательным для назначения является параметр “_table”, остальные могут быть назначены при необходимости. Список значений alias, доступных для назначения в теге <value-field>, является фиксированным.
Также в каждом теге, описывающем поле value-field, необходимо обязательно добавлять атрибут service=”true”, указывающий, что данное поле является служебным.
В случае если для поля не были указаны иные атрибуты, кроме “_table”, это означает, что в данной таблице присутствуют сущности ТОЛЬКО указанного типа (нет необходимости различать сущности разного типа). В этом случае при загрузке каждой отдельной записи из таблицы будет считаться, что её _typeId равен тому, который указан в названии сущности.
Описание текстового поля имеет следующий синтаксис:
<text-field alias=”textField” common=”true|false” description=“descriptionField”>text_field</text-field>
<document-field alias=”fieldAlias” typeId=”user”>
<formula-long-field alias="needProcess" lazy="true" description="Вычисляемое поле. Наличие необработанных объектов"> <formula>(SELECT 1 FROM table1 d JOIN table2 t ON d.id = t.doc_id WHERE d.status_id = 1 AND t.upload_id = ${alias}.id )</formula> </formula-long-field>
Позволяет использовать разные поля данной сущности в зависимости от условия;
Пример использования: отображать поле в зависимости от языковых настроек. Сущность можно описать следующим образом:
<meta typeId="subst-entity" versionId="0" actual="true"> <fields> <value-field alias="_schema">tmp</value-field> <value-field alias="_table">subst_entities</value-field> <value-field alias="_id">id</value-field> <text-field alias="docNum" common="true">doc_num</text-field> <text-field alias="valueRu" common="true" lazy="true">value_ru</text-field> <text-field alias="valueEn" common="true" lazy="true">value_en</text-field> <text-field alias="valueDef" common="true" lazy="true">value_def</text-field> <substitute-text-field alias="substField" lazy="true"> <condition language="js">context.getLocale()</condition> <if value="ru">valueRu</if> <if value="en">valueEn</if> <default>valueDef</default> </substitute-text-field> </fields> </meta>
При создании связи «многие ко многим» (many-to-many, m2m) в теге <m2m> необходимо указать значение атрибута joinTable (joinTable=”table_name”), указав таким образом имя промежуточной таблицы (реально существующей таблицы в БД).
Далее внутри тега <sources> необходимо в теге <field> указать название поля сущности, из которой устанавливается связь (alias) и колонку промежуточной таблицы (column), используемую для формирования связи с одной из исходных таблиц (например, <field alias="usrId" column="column_one"/ >). Аналогично внутри тега <destinations> необходимо в теге <field> указать название поля сущности, к которой устанавливается связь (alias) и колонку таблицы (column), используемую для формирования связи со второй из исходных таблиц (например, <field alias="_id" column="column_second"/>). Важно, что обе указанные колонки (в <sources> и в <destinations>) должны быть реально существующими колонками промежуточной таблицы table_name.
NB: Здесь присутствует определённое расхождение в понятиях, так как в alias указывается название поля (не колонки), а в column – название колонки. Это сделано для упрощения: не требуется заводить отдельный маппинг промежуточной таблицы.
restrictions public interface Restrictions { public Restriction eq(String field, @Nullable Object value); public Restriction same(String field, String otherField); public Restriction ge(String field, Object value); public Restriction gt(String field, Object value); public Restriction le(String field, Object value); public Restriction lt(String field, Object value); public Restriction ne(String field, Object value); public Restriction or(Restriction... restrictions); /** convenient method or for execute from js */ public Restriction or(Restriction restriction1, Restriction restriction2); public Restriction and(Restriction... restrictions); public Restriction commaSeparated(Restriction... restrictions); public Restriction in(String field, Object... values); public Restriction in(String[] fields, IEntityQuery query); public Restriction isNull(String field); public Restriction isNotNull(String field); /** This method is analogue in and you have to use it instead in from js */ public Restriction present(String field, Object... values); public Restriction present(String field, Set values); public Restriction present(String[] fields, IEntityQuery query); public Restriction id(Object value); public Restriction id(String alias, Object value); public Restriction versionId(Object value); public Restriction versionId(String alias, Object value); public Restriction typeId(Object value); public Restriction typeId(String alias, Object value); public Restriction actual(boolean value); public Restriction actual(String alias, boolean value); public Restriction deleted(boolean value); public Restriction deleted(String alias, boolean value); public Restriction not(Restriction restriction); public Restriction like(String field, String pattern); public Restriction like(String alias, String field, String pattern); public Restriction ilike(String field, String pattern); public Restriction ilike(String alias, String field, String pattern); public Restriction field(String alias); public Restriction field(String alias, String field); public Restriction field(String alias, Field field); public Restriction field(String alias, String field, String resultAlias); public Restriction hasAccess(IUser user); public Restriction hasAccess(String alias, IUser user); public Restriction hasBit(String alias, int value); /* xml-related restrictions */ public Restriction hasTag(String alias, String field, String xpath); public Restriction hasTag(String field, String xpath); }
storage.query("user").add(restrictions.like("surname", "А%")).list()
Синтаксис: storage.query("user").join("entity", "en", "STRICT/WEAK", restrictions.r()), где «entity» - имя сущности, с которой выполняется join, “en” – псевдоним для сущности, “STRICT”/”WEAK” – режим объединения (“STRICT” эквивалентен inner join, “WEAK” – outer join).
На создаваемое объединение могут накладываться ограничения (restrictions), как и на отображение результатов запросов.
Пример: Посчитать всех пользователей, которые имеют роль «Администратор» (roles._id = 1), которых зовут Анастасия, и всех пользователей по имени Алексей, у которых нет роли «Администратор».
В дальнейшем предоставляется возможность накладывать ограничения на результат объединения.storage.query("user").join("roles", "r", "WEAK", restrictions.eq("r.name", "admin")).add(restrictions.or( restrictions.and([restrictions.eq("r._id", 1), restrictions.like("name", "%Анастасия%")]), restrictions.and([restrictions.isNull("r.name"), restrictions.like("name", "%Алексей%")]))).count();
Объединение с использованием функции use() эквивалентно outer join. Синтаксис данного вида объединения выглядит следующим образом:
storage.query("table1").use(["table2", restrictions.add("param", value)])
storage.query("table1").strict(["table2", restrictions.add("param", value)])
storage.query("user").strict(["roles"]).count()
storage.query("role").use(["users"]).add(restrictions.isNull("users._id")).list()
fetch() – работа с lazy полями.
Примеры работы:storage.query("user").add(restrictions.like("surname", "А%")).list()
storage.query("user").use(["roles", "roles.parent"]).add(restrictions.or(restrictions.eq("roles._id", 4), restrictions.eq("roles.parent._id", 37))).list()
storage.query("user").join("roles", "roles", "STRICT").join("roles.parent", "parent", "WEAK").add(restrictions.or(restrictions.eq("roles._id", 4), restrictions.eq("parent._id", 37))).list()
storage.query("role").use(["users"]).add(restrictions.isNull("users._id")).list()
storage.query("user").add(restrictions.like("name", "Алексей")).list(0, 9)
storage.query("role").count()
storage.query("role").use(["users"]).add(restrictions.like("users.name", "%Иван%")).count()
storage.query("user").add(restrictions.isNotNull("name")).sort("fio", "desc").one().getField("fio")
storage.query("user").use(["roles"]).add(restrictions.or(restrictions.eq("roles._id", 1), restrictions.like("name", "%Анастасия%"))).list()
storage.query("user").join("roles", "r", "WEAK", restrictions.eq("r.name", "admin")).add(restrictions.or( restrictions.and([restrictions.eq("r._id", 1),restrictions.like("name", "%Анастасия%")]), restrictions.and([restrictions.isNull("r.name"),restrictions.like("name", "%Алексей%")]))).count()
storage.query("user").join("roles", "r", "WEAK", restrictions.eq("r.name", "admin")).add(restrictions.or( restrictions.and([restrictions.eq("r._id", 1),restrictions.like("name", "%Анастасия%")]), restrictions.and([restrictions.isNull("r.name"),restrictions.like("name", "%Алексей%")]))).fields(“fio”)
По маршруту можно запустить любой объект, у которого есть метаданные. Не всякий описанный метаданными объект должен быть запущен по маршруту.
Workflow может состоять из нескольких видов элементов. Рассмотрим их подробнее.
В параметрах данного блока разработчику предоставляется возможность добавить/изменить описание блока и написать код, который будет исполняться в блоке.
Во многих случаях код в блоке завершается командой go или goSelf (например, self.goSelf("out", user, null, param, {}); команда goSelf, в отличие от go,при выполнении не порождает новый поток) – с помощью этой команды осуществляется переход в следующий блок с передачей в него параметров, передаваемых в качестве параметров команды.
В качестве завершения также может использоваться команда setAnswer (например, self.setAnswer(param); ), данная команда разблокирует вызывающий поток, передавая ему результат выполнения кода. Следует обратить внимание, что после setAnswer может выполняться другой код
Для UserActionWaitFlow можно задать параметры leave in и leave through. В параметре leave in указывается время до автоматического выхода из блока (в минутах); в параметре leave through – через какой коннектор осуществляется автоматический выход из блока.
Предположим, что требуется автоматизировать при помощи настоящего программного обеспечения следующие процесс работы с некоторым протоколом.
Параметры протокола представлены в таблице ниже:Номер протокола | Число |
Дата протокола | Дата |
ФИО проверяемого | Строка |
Дата рождения проверяемого | Дата |
Полное название организации осуществлявшей проверку | Строка |
Квалификационная категория | Число (1-4) |
Дата проверки | Дата |
ФИО проводившего проверку | Строка |
Должность проводившего проверку | Строка |
Количество заданных вопросов | Число |
Количество данных правильно ответов | Число |
Количество ошибок при проведении практического теста | Число |
Результат проверки | Да/Нет |
Начальник управления | Строка |
<?xml version="1.0" standalone="no"?> <metadata xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="metadata.xsd"> <meta typeId="exam-protocol"> <fields> <value-field alias="_id">id</value-field> <value-field alias="_table">exam_protocols</value-field> <value-field alias="_typeId">type_id</value-field> <value-field alias="_versionId">version_id</value-field> <long-field alias="protocolId" common="true">protocol_id</long-field> <date-time-field alias="protocolDate" common="true">protocol_date</date-time-field> <text-field alias="verifiableName" common="true">verifiable_name</text-field> <date-time-field alias="verifiableBirthDate" common="true">verifiable_birth_date</date-time-field> <text-field alias="auditOrgName" common="true">audit_org_name</text-field> <text-field alias="category" common="true">category</text-field> <date-time-field alias="auditDate" common="true">audit_ate</date-time-field> <text-field alias="auditorName" common="true">auditor_name</text-field> <text-field alias="auditorJob" common="true">auditor_job</text-field> <long-field alias="questionsCount" common="true">questions_count</long-field> <long-field alias="answersCount" common="true">answers_count</long-field> <long-field alias="errorsCount" common="true">errors_count</long-field> <boolean-field alias="auditResult" common="true">audit_result</boolean-field> <text-field alias="auditorBoss" common="true">auditor_boss</text-field> </fields> </meta> </metadata>
<document-field alias="attachments"typeId="file" cascade="store"> <m2m joinTable="document_files" aliasColumn="document_field"> <sources> <field alias="_id" column="document_id"/> <field alias="_versionId" column="document_version_id"/> <field alias="_typeId" column="document_type_id"/> </sources> <destinations> <field alias="_id" column="file_id"/> <field alias="_versionId" column="file_version_id"/> <field alias="_typeId" column="file_type_id"/> </destinations> <fields> <long-field alias="index" common="true">idx</long-field> </fields> </m2m> </document-field>
<document-field alias="events" typeId="event"> <mapped relationship="O2M" mappedBy="document"/> </document-field> <document-field alias="eventsSummary" typeId="events-summary"> <m2o> <field sourceAlias="_id" destinationAlias="paramId"/> <field sourceAlias="_typeId" destinationAlias="paramTypeId"/> </m2o> </document-field>
if (document != null) { out.add(convert.document(storage.query(document.getTypeId()) .use(["attachments"]) .add(restrictions.id(document.getId())) .one()), 0); }
Создаем новый маршрут, назовем его «exam-protocol-automated» Добавляем его внутри «SuperFlow» потому как он обязателен, внутри него создаются остальные маршруты. Создаем «Add ActionFlow», соединяем коннектором входы (зеленый кружек) SuperFlow и ActionFlow. Далее добавляем «Add UserActionWaitFlow» (eventListenerFlow), соеденяем выход(красный кружек) ActionFlow и вход eventListenerFlow и задаем ему «title»: out, «service»: true (для этого два раза кликаем на красном кружке выхода ActionFlow) В поле code для ActionFlow пишем следующее:
// make new exam-protocol: var param = storage.make("exam-protocol"); // setting fields: param.setField("protocolId", ctx.get("protocolId")); param.setField("protocolDate", ctx.get("protocolDate")); param.setField("verifiableName", ctx.get("verifiableName")); param.setField("verifiableBirthDate", ctx.get("verifiableBirthDate")); param.setField("auditOrgName", ctx.get("auditOrgName")); param.setField("category", ctx.get("category")); param.setField("auditDate", ctx.get("auditDate")); param.setField("auditorName", ctx.get("auditorName")); param.setField("auditorJob", ctx.get("auditorJob")); param.setField("questionsCount", ctx.get("questionsCount")); param.setField("answersCount", ctx.get("answersCount")); param.setField("errorsCount", ctx.get("errorsCount")); param.setField("auditResult", ctx.get("auditResult")); param.setField("auditorBoss", ctx.get("auditorBoss")); // store attachments $("updateAttachments")(param, ctx.get("attachments")); // store document: param = storage.store(param); // continue execution: self.goSelf("out", null, $("getRoleByName")("everyone"), param, {});
param = storage.query(param.getTypeId()). add(restrictions.id(param.getId())). use(["eventsSummary"]). one(); sender.sendEvent(param, {role: role}, event); // return created document: self.setAnswer(param);
Далее добавим маршрут для обновления протокола. Создадим новый actionFlow с «description»: “update”. Соединим выход eventListenerFlow(“new”) со входом actionFlow(“update”) и зададим ему параметры «title»: save, «saves state»: true
В actionFlow(“update”) пишем следующее:// setting fields: param.setField("protocolId", ctx.get("protocolId")); param.setField("protocolDate", ctx.get("protocolDate")); param.setField("verifiableName", ctx.get("verifiableName")); param.setField("verifiableBirthDate", ctx.get("verifiableBirthDate")); param.setField("auditOrgName", ctx.get("auditOrgName")); param.setField("category", ctx.get("category")); param.setField("auditDate", ctx.get("auditDate")); param.setField("auditorName", ctx.get("auditorName")); param.setField("auditorJob", ctx.get("auditorJob")); param.setField("questionsCount", ctx.get("questionsCount")); param.setField("answersCount", ctx.get("answersCount")); param.setField("errorsCount", ctx.get("errorsCount")); param.setField("auditResult", ctx.get("auditResult")); param.setField("auditorBoss", ctx.get("auditorBoss")); // store attachments $("updateAttachments")(param, ctx.get("attachments")); //store doc param = storage.update(param); //return doc self.setAnswer(param);
Далее выберем eventListenerFlow(“new”) и добавим новый коннектор(Add Workflow -> Append connector) и зададим ему «title»: “delete” , а поле code добавим следующее(посылаем событие “out:delete”):
// send notification to delete document from registry: sender.sendEvent(param, {role: $("getRoleByName")("everyone")}, "out:" + event);); //return doc self.setAnswer(param);
Еще раз выберем eventListenerFlow(“new”) и добавим новый коннектор(Add Workflow -> Append connector) и зададим ему «title»: “next” , а поле code добавим следующее(посылаем событие “out:next”):
// send notification to delete document from registry: sender.sendEvent(param, {role: $("getRoleByName")("everyone")}, "out:" + event);
Создадим новый actionFlow с «description»: “deleted”. Соединим коннектор eventListenerFlow(“new”) («title»: “delete”) со входом actionFlow(“deleted”) В поле code для actionFlow(“deleted ”) пишем следующее:
param = storage.remove(param); //return doc self.setAnswer(param);
Создадим новый eventListenerFlow с «description»: “review”. Соединим коннектор eventListenerFlow(“new”)(«title»: “next”) и eventListenerFlow(“review”) В поле code для eventListenerFlow(“review”) напишем:
param.fetch(["eventsSummary"]); sender.sendEvent(param, {role: $("getRoleByName")("everyone")}, event); // return created document: self.setAnswer(param);
en.getData().setField("_typeId", "exam-protocol"); en.getData().setField("_workflow", "exam-protocol-automated");
out.setField("_code", "alias"); out.setField("_description", "name"); storage.query("document-events").list(); storage.query("exam-protocol").use(["eventsSummary"]).list();
listeners.add( [{ role: $("getRoleByName")("everyone") }], eventRestrictions.and([ eventRestrictions.eventIn(["new", "out:new", "review", "out:review"]), eventRestrictions.eq("_typeId", "exam-protocol") ]), listener (en) { var event = en.getData().getFieldAsString("_event", "-"); en.alert("event: " + event); importer.get("updateRegistry")(en, "protocolTable", "_param.", false); });