Демо микросервис

ecos-demo-app — эталонный микросервис платформы Citeck, предназначенный для разработчиков, которые создают собственные микросервисы или интегрируют внешние системы с платформой. Приложение написано на Java с использованием Spring Boot и охватывает наиболее распространённые сценарии работы с ECOS API.

Документ адресован разработчикам (Java/Kotlin), которые начинают работу с платформой Citeck ECOS или хотят разобраться в типовых паттернах интеграции.

В демо-приложении реализованы следующие механизмы платформы:

  • Records API — пользовательский RecordsDAO с хранением данных в памяти и полным набором CRUD-операций;

  • Actions — серверные действия, включая отправку email-уведомлений и создание дочерних сущностей по действию;

  • Commands — асинхронный обмен сообщениями между микросервисами;

  • Jobs — планирование регулярного выполнения задач с помощью аннотации @Scheduled;

  • Events — подписка на события платформы (создание и изменение записей) с поддержкой транзакционных и асинхронных слушателей;

  • External Tasks — выполнение задач BPMN-процесса во внешнем сервисе;

  • BPMN-процесс — полный цикл бизнес-процесса с автостартом, пользовательской задачей и внешней задачей.


Репозиторий ecos-demo-app содержит приложение, демонстрирующее возможности Citeck.

Вы можете познакомиться с:

См. подробнее про артефакты Citeck, приложения.

При запуске приложения в левом меню по умолчанию появится раздел «Демо тип/Demo type» с двумя журналами:

  • Журнал демо типа/Demo type journal — основной демо журнал с процессом BPMN и различными демонстрационными действиями. Написанный ниже сценарий основан на этом типе.

  • in-memory демо тип/Demo in-memory type — журнал с сущностями в памяти для демонстрации работы с пользовательским RecordsDAO, определенным в ru.citeck.ecos.webapp.demo.records.DemoInMemRecordsDao. Вы можете создавать/просматривать/редактировать/удалять записи в этом журнале и смотреть, что изменилось в DemoInMemRecordsDao.

С чего начать

Если вы не знакомы с платформой Citeck, и хотите запустить программное обеспечение локально, мы рекомендуем вам загрузить Dockerized версию Community demo. Подробно об установке.

Микросервис написан с использованием Spring Boot.

Примечание

Для запуска этого приложения необходимы следующие приложения из развертывания Citeck:

  • zookeeper;

  • rabbitmq;

  • ecos-model;

  • ecos-registry.

Запуск

Клонируйте репозиторий в свою среду разработки. Для запуска приложения выполните:

./mvnw spring-boot:run

Если ваша IDE поддерживает запуск приложений Spring Boot напрямую, вы можете легко запустить класс ru.citeck.ecos.webapp.demo.EcosDemoApp без дополнительной настройки.

Сценарий работы

  1. Запустите ecos-demo-app.

  2. В Citeck в верхнем левом углу нажмите «Создать/Create».

  3. Выберите «Демо тип/Demo type» -> «Демо тип/Demo type».

  4. Введите в поле «Имя/Name» значение «ошибка» и нажмите кнопку «Сохранить/Save». Вы должны увидеть ошибку от транзакционного listener, определенного в ru.citeck.ecos.webapp.demo.events.DemoEcosEventListener.

  5. Измените значение поля «Имя/Name» на любое другое и заполните остальные поля.

  6. После создания вы увидите информацию о созданной записи:

  • Статус будет «Новый/New». Это определено в свойстве defaultStatus в конфигурации типа — src/main/resources/eapps/artifacts/model/type/demo-type.yml

  • Виджеты задач будут отображать активную задачу для текущего пользователя. Процесс BPMN запущен, поскольку у нас есть определение процесса в src/main/resources/eapps/artifacts/process/bpmn/demo-process.bpmn.xml с флагами ecos:enabled=»true» и ecos:autoStartEnabled=»true».

  1. Нажмите кнопку «Готово/Done» в виджете текущей задачи.

  2. Задача исчезнет и будет запущена внешняя задача — ru.citeck.ecos.webapp.demo.exttask.DemoExternalTask.

  3. Примерно через 5–10 секунд вы сможете обновить вкладку браузера и увидеть новый статус «Завершенный/Completed» и заполненное поле «Поле сгенерированное во внешней задаче/Field generated in external task». На этом этапе процесс BPMN завершается.

  4. Вы можете нажать «Отправить демо письмо/Send demo email», чтобы протестировать специальное действие для отправки электронного письма.

  • Класс действия: ru.citeck.ecos.webapp.demo.actions.SendDemoEmailAction

  • Определение действия: src/main/resources/eapps/artifacts/ui/action/send-demo-email-action.yml

  • Шаблон электронного письма: src/main/resources/eapps/artifacts/notification/template/demo-email.html.ftl.

  • Письмо с результатом можно найти в mailhog (если вы не меняли настройки электронной почты по умолчанию) — http://localhost:8025/

  1. После тестирования отправки письма вы можете нажать «Создать дочернюю сущность/Create child entity», чтобы проверить возможность создания связанных объектов по действию.

  • Определение действия: src/main/resources/eapps/artifacts/ui/action/create-child-entity-action.yml

Описание сущностей микросервиса

Артефакты

В папке .../src/main/resources/eapps/artifacts расположены артефакты проекта. Первые два уровня каталогов соответствуют типу артефакта. Например:

  • app/artifact-patch

  • model/type

  • notification/template

  • process/bpmn

  • ui/action, /form, /journal

Подробнее про артефакты Citeck

Классы

Запись (Record) – сущность с набором атрибутов и идентификатором записи (RecordRef).

Ниже разобран простой пример RecordsDAO с хранением сущностей в памяти. Данный RecordsDAO демонстрирует простые базовые операции CRUD в API Records и не реализует такие функции, как ассоциации, хранение контента, проверку разрешений и т. д. См. подробное описание операций CRUD

Ссылка на Records в репозитории Git

Предупреждение

Все данные будут потеряны после перезапуска приложения. Не используйте для продакшн-среды.

@Component
public class DemoInMemRecordsDao extends AbstractRecordsDao
        implements RecordsQueryDao, RecordAttsDao, RecordMutateDao, RecordDeleteDao {

    public static final String ID = "demo-inmem-data";

    /**
    * Создание простого хранилища для записей. Все данные будут потеряны после рестарта приложения.
    */
    private final Map<String, SimpleDto> records = new ConcurrentHashMap<>();

    /**
    * Запрос Query records поддерживает только язык «предикатов».
    * @param recordsQuery – параметры запроса, отправляемые с фронтенда
    * @return найденные записи и информацию об общем количестве без пагинации
    */
    @Nullable
    @Override
    public RecsQueryRes<?> queryRecords(@NotNull RecordsQuery recordsQuery) {

        // О предикатах подробно можно прочитать по ссылке
        // https://citeck-ecos.readthedocs.io/ru/latest/general/%D0%AF%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%B5%D0%B4%D0%B8%D0%BA%D0%B0%D1%82%D0%BE%D0%B2.html
        if (!PredicateService.LANGUAGE_PREDICATE.equals(recordsQuery.getLanguage())) {
            return null;
        }

        Predicate predicate = recordsQuery.getQuery(Predicate.class);

        QueryPage page = recordsQuery.getPage();
        List<SimpleDto> fullResult = predicateService.filterAndSort(
                records.values(),
                predicate,
                recordsQuery.getSortBy(),
                page.getSkipCount(),
                page.getMaxItems()
        );

        RecsQueryRes<SimpleDto> recsQueryRes = new RecsQueryRes<>();
        recsQueryRes.setTotalCount(records.size());
        recsQueryRes.setRecords(fullResult);

        return recsQueryRes;
    }

    /**
    * Получить данные рекорда по localId.
    * @return сам рекорд или null
    */
    @Nullable
    @Override
    public Object getRecordAtts(@NotNull String localId) {
        return records.get(localId);
    }

    /**
    * Создание или обновление рекорда.
    * Если recordAtts.getId() пустая строка, то создается новый рекорд со сгенерированным localId
    * @param recordAtts localId (String) и key-value map (ObjectData) с атрибутами сущности
    * @return localId существующего или созданного рекорда.
    */
    @NotNull
    @Override
    public String mutate(@NotNull LocalRecordAtts recordAtts) {
        SimpleDto recToMutate;
        if (recordAtts.getId().isEmpty()) { //создание
            // Обычно в других не демо-версиях RecordsDao, когда getId() пустой,
            // id можно указать в атрибутах, но здесь мы не реализуем эту логику.
            // Вы можете просмотреть исходный код ru.citeck.ecos.records3.record.dao.impl.mem.InMemDataRecordsDao
            // чтобы проверить правильную реализацию метода mutate.
            recToMutate = new SimpleDto(UUID.randomUUID().toString());
        } else {
            recToMutate = records.get(recordAtts.getId()); // обновление
            if (recToMutate == null) {
                throw new IllegalArgumentException("Record with id " + recordAtts.getId() + " is not found");
            }
            recToMutate = new SimpleDto(recToMutate);
            recToMutate.modified = Instant.now();
        }
        Json.getMapper().applyData(recToMutate, recordAtts.getAtts());
        if (recToMutate.id.isBlank()) {
            throw new IllegalArgumentException("Record id is empty after mutation. Atts: " + recordAtts);
        }
        records.put(recToMutate.id, recToMutate);
        return recToMutate.id;
    }

    /**
    * Удаление определенных рекордов.
    * @param localId удаленной записи
    * @return localId существующего или созданного рекорда.
    */
    @NotNull
    @Override
    public DelStatus delete(@NotNull String localId) {
        records.remove(localId);
        return DelStatus.OK;
    }

    @NotNull
    @Override
    public String getId() {
        return ID;
    }

    /**
    * Создание DTO
    */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class SimpleDto {

        private String id;
        private String textField;
        private int numField;

        private Instant created;
        private Instant modified;

        public SimpleDto(String id) {
            this.id = id;
            created = Instant.now();
            modified = created;
        }

        public SimpleDto(SimpleDto other) {
            this.id = other.id;
            this.textField = other.textField;
            this.numField = other.numField;
            this.created = other.created;
            this.modified = other.modified;
        }

        /**
        * Данный метод вызывается, когда скаляр '?disp' загружается из рекорда.
        * Подробно о скалярах https://citeck-ecos.readthedocs.io/ru/latest/general/ECOS_Records.html#id11
        * 'getDisplayName' специальное имя в DTO для скаляра '?disp'
        */
        public MLText getDisplayName() {
            Map<Locale, String> nameData = new HashMap<>();
            nameData.put(I18nContext.ENGLISH, "Demo in-mem '" + textField + "'");
            nameData.put(I18nContext.RUSSIAN, "Демо in-mem '" + textField + "'");
            return new MLText(nameData);
        }

        /**
        * Данный метод вызывается, когда атрибут '_type' attribute загружается из рекорда.
        * 'getEcosType' специальное имя в DTO  для атрибута DTO for '_type' .
        * Движок добавляет дополнительную логику для данного метода:
        * Если результат метода EntityRef или строка, начинающаяся с 'emodel/type@', то результат будет EntityRef.valueOf(methodResult).
        * Если результат метода строка и она не начинается с 'emodel/type@', то движок добавляет префикс 'emodel/type@' и возвращает результат EntityRef.valueOf.
        */
        public String getEcosType() {
            // type config: src/main/resources/eapps/artifacts/model/type/demo-inmem-type.yaml
            return "demo-inmem-type";
        }

        /**
        * Простой геттер для атрибута _created.
        * Аннотация AttName требуется, чтобы изменить имя атрибута по умолчанию "created" на "_created".
        * '_created' специальный мета атрибут для любого рекорда, который должен информировать, когда рекорд был создан.
        */
        @AttName(RecordConstants.ATT_CREATED)
        public Instant getCreated() {
            return created;
        }

        /**
        * Простой геттер для атрибута _modified.
        * Аннотация AttName требуется, чтобы изменить имя атрибута по умолчанию "modified" на "_modified".
        * '_modified' специальный мета атрибут для любого рекорда, который должен информировать, когда рекорд был изменен последний раз.
        */
        @AttName(RecordConstants.ATT_MODIFIED)
        public Instant getModified() {
            return modified;
        }
    }
}

Сборка

Для сборки docker образа с микросервисом выполните команду:

./mvnw -Pprod clean package jib:dockerBuild -Djib.docker.image.tag=custom

После сборки вы можете запустить контейнер ecos-demo-app:custom с помощью docker.

Тестирование

Для запуска тестов вашего приложения, выполните:

./mvnw clean test