Поддержка аддона Rest DataStore 2.4 со старым аддоном Rest API (1.3)

Коллеги, приветствую!

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

Ситуация:

  • Клиентское приложение: Jmix 2.4 с аддоном REST DataStore
  • Серверное приложение: Jmix 1.3 с аддоном REST API

Вопросы:

  1. Какие ограничения и потенциальные проблемы могут возникнуть при таком взаимодействии?
  2. Поддерживается ли вообще совместимость между Rest DataStore 2.4 и Rest API 1.3?
  3. Есть ли рекомендуемые подходы для организации такого взаимодействия между разными версиями?

Артем, добрый день!

Ответил вам на англоязычном форуме: Compatibility Between REST DataStore (Jmix 2.4) and REST API (Jmix 1.3) - #3 by krivopustov - Support - Jmix

С уважением,
Константин

Константин, добрый день!

REST API в Jmix 1.x поддерживает только grant type password . Не могли бы вы дать рекомендации, как можно реализовать собственную поддержку grant type client_credentials в Jmix 1.x?

Лично я к сожалению не могу дать рекомендаций. Мы официально не развиваем REST в 1.x, поэтому не могу выделить время на это.

Можете помочь разобраться с подгрузкой атрибутов сущностей через fetch-plan на экране?

Я добавил новый класс Order:

@Table(name = "ORDER_", indexes = {
        @Index(name = "IDX_ORDER__REF_CUSTOMER", columnList = "REF_CUSTOMER_ID")
})
@Entity(name = "Order_")
public class Order {
    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private Integer version;

    @InstanceName
    @Column(name = "NAME")
    private String name;

    @OnDeleteInverse(DeletePolicy.CASCADE)
    @JoinColumn(name = "REF_CUSTOMER_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private Customer refCustomer;

    // Геттеры и сеттеры
    // ...
}

А в класс Customer добавил соответствующую связь:

private List<Order> refOrders;

public List<Order> getRefOrders() {
    return refOrders;
}

public void setRefOrders(List<Order> refOrders) {
    this.refOrders = refOrders;
}

Создал fetch-plans:

<fetchPlans xmlns="http://jmix.io/schema/core/fetch-plans">
    <fetchPlan entity="Order_" name="order-fetch-plan" extends="_base">
        <property name="refCustomer" fetchPlan="_base"/>
    </fetchPlan>
    <fetchPlan entity="Customer" name="customer-fetch-plan" extends="_base">
        <property name="refOrders" fetchPlan="_base"/>
    </fetchPlan>
</fetchPlans>

Теперь в клиенте создал DTO Order и добавил новое поле в Customer :

@Store(name = "serviceapp")
@RestDataStoreEntity(remoteName = "Order_")
public class Order {
    @JmixGeneratedValue
    @JmixId
    private UUID id;

    private Integer version;

    @InstanceName
    private String name;

    @OnDeleteInverse(DeletePolicy.CASCADE)
    private Customer refCustomer;

    // Геттеры и сеттеры
    // ...
}

@JmixEntity
@Store(name = "serviceapp")
public class Customer {
    @JmixGeneratedValue
    @JmixId
    private UUID id;

    private Integer version;

    @InstanceName
    private String name;

    private String email;

    private List<Order> refOrders;

    // Геттеры и сеттеры
    // ...
}

Проблема:
При отображении данных в экране поле order.refCustomer не подгружается. И при обращение к полю customer.refOrders. И появляется ошибка io.jmix.restds.exception.InvalidFetchPlanException: Data store 'serviceapp' supports only named fetch plans defined in fetch plans repository.

Вопрос:
Как правильно работать с fetch-plan на стороне клиента? И еще можете подсказать, какие аннотация в клиенте нужно убирать из атрибутов сущности?

Пример экрана на клиенте:

<window xmlns="http://jmix.io/schema/ui/window"
        xmlns:c="http://jmix.io/schema/ui/jpql-condition"
        caption="msg://orderBrowse.caption"
        focusComponent="ordersTable">
    <data readOnly="true">
        <collection id="ordersDc"
                    class="com.company.serviceapp.entity.Order">
            <fetchPlan extends="_base">
                <property name="refCustomer" fetchPlan="_base"/>
            </fetchPlan>
            <loader id="ordersDl">
                <query>
                    <![CDATA[select e from Order_ e]]>
                </query>
            </loader>
        </collection>
    </data>
    <facets>
        <dataLoadCoordinator auto="true"/>
        <screenSettings id="settingsFacet" auto="true"/>
    </facets>
    <layout expand="ordersTable" spacing="true">
        <filter id="filter" dataLoader="ordersDl">
            <properties include=".*"/>
        </filter>
        <groupTable id="ordersTable"
                    width="100%"
                    dataContainer="ordersDc">
            <columns>
                <column id="name"/>
                <column id="refCustomer"/>
            </columns>
            <!-- ... -->
        </groupTable>
    </layout>
</window>

Артем, добрый день!
Спасибо что затронули этот важный момент.

Jmix 1.x REST не поддерживает встроенные фетч-планы, поэтому вы во-первых должны объявить нужные фетч-планы и на сервисе и на клиенте, а во-вторых использовать их на клиенте по имени, например:

<collection id="ordersDc"
                    class="com.company.serviceapp.entity.Order"
                    fetchPlan="order-fetch-plan">

Элемент <fetchPlan> нужно вообще удалить из <collection>.

Проект jmix-projects/jmix-restds-compatibility обновлен - там теперь есть пример использования именованных фетч-планов.

По поводу аннотаций - удаляйте все JPA аннотации пакета jakarta.persistence.

С уважением,
Константин

Константин, большое спасибо за помощь :handshake:t2:

Константин, хочу уточнить возможность использования Rest DataStore в следующем сценарии:

  1. Имеется:
  • Отдельный сервис авторизации (authservice) на Jmix 1.x
  • Несколько сервисов (serviceapp1, serviceapp2), также на Jmix 1.x
  1. Желаемый flow:
  • Клиент авторизуется в authservice и получает токен
  • С этим токеном обращается к API других сервисов

Вопрос:
Поддерживает ли Rest DataStore в Jmix 1.x настройку, где:

  • Авторизация происходит только через authservice
  • Полученный токен автоматически используется для запросов к другим сервисам (serviceapp1, serviceapp2)?

Или требуется реализовывать это кастомно?

Константин, хочу уточнить реализацию для еще одного случая :sweat_smile:, когда нужно настроить доступ к REST API в нескольких сервисах (serviceapp , serviceapp2 ), каждый со своей аутентификацией.

Предполагаемое решение:

  1. Конфигурация в application.properties:
# Общие настройки multi-store
jmix.core.additional-stores=serviceapp,serviceapp2
jmix.core.store-descriptor-serviceapp=restds_RestDataStoreDescriptor
jmix.core.store-descriptor-serviceapp2=restds_RestDataStoreDescriptor

# Настройки serviceapp
serviceapp.baseUrl=http://localhost:8071
serviceapp.clientId=client
serviceapp.clientSecret=secret
serviceapp.authenticator=restds_RestPasswordAuthenticator
serviceapp.tokenPath=/oauth/token

# Настройки serviceapp2
serviceapp2.baseUrl=http://localhost:8072
serviceapp2.clientId=client
serviceapp2.clientSecret=secret
serviceapp2.authenticator=restds_RestPasswordAuthenticator
serviceapp2.tokenPath=/oauth/token

# Учетные данные
integration.serviceapp.store=serviceapp
integration.serviceapp.username=admin
integration.serviceapp.password=admin

integration.serviceapp2.store=serviceapp2
integration.serviceapp2.username=admin
integration.serviceapp2.password=admin
  1. Кастомный IntegrationInitializer:
@Component
public class IntegrationInitializer {
    @Value("${integration.serviceapp.store}") 
    private String serviceAppStore;
    
    @Value("${integration.serviceapp.username}")
    private String serviceAppUser;
    
    @Value("${integration.serviceapp.password}")
    private String serviceAppPassword;
    
    @Value("${integration.serviceapp2.store}")
    private String serviceApp2Store;
    
    @Value("${integration.serviceapp2.username}")
    private String serviceApp2User;
    
    @Value("${integration.serviceapp2.password}")
    private String serviceApp2Password;

    @EventListener
    public void onApplicationStarted(ApplicationStartedEvent event) {
        authenticateStore(event, serviceAppStore, serviceAppUser, serviceAppPassword);
        authenticateStore(event, serviceApp2Store, serviceApp2User, serviceApp2Password);
    }

    private void authenticateStore(ApplicationStartedEvent event, 
                                 String storeName, 
                                 String username, 
                                 String password) {
        RestPasswordAuthenticator authenticator = event.getApplicationContext()
            .getBean(RestPasswordAuthenticator.class);
        authenticator.setDataStoreName(storeName);
        authenticator.authenticate(username, password);
    }
}

Вопросы:

  1. Корректна ли такая реализация для работы с несколькими REST-сервисами?
  2. Нужно ли дополнительно конфигурировать RestDataStore для каждого сервиса?
  3. Возможны ли подводные камни при таком подходе (например, конфликты токенов)?

Проблема с несколькими REST-сервисами будет в хранении токена в объекте IntegrationTokenHolder. В текущей реализации проекта jmix-restds-compatibility/client-app at main · jmix-projects/jmix-restds-compatibility · GitHub это синглтон-бин, хранящий токен в своем поле. То есть он подходит для интеграции с единственным сервисом.

Для работы с несколькими сервисами придется переопределить бин RestPasswordAuthenticator и реализовать выбор TokenHolder для того REST DataStore, с которым работает данный authenticator. Кажется что в текущем коде можно переопределить метод setDataStoreName(String name) и в нем инициализировать “свой” TokenHolder.

Мы подумаем как сделать это удобнее в следующей версии фреймворка (тикет).

Добрый день! Понял)

А на счет этого вопроса:
image

Хотел уточнить, что тут авторизация в authservice уже планируется через тип гранта Client Credentials (Мы ее самостоятельно реализовали)

Это похоже на конфигурацию с внешней аутентификацией, которая описана в руководстве REST DataStore with External Authentication :: Документация Jmix.

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

Константин, спасибо большое, что ответили на все вопросы!