All Articles ↓
3 недели назад
CUBA Platform: TypeScript SDK и REST API

В этой статье мы рассмотрим давно существующий, но почему-то не очень широко известный инструмент CUBA Platform - генератор SDK для фронт-энда. А также рассмотрим, как он работает в комбинации с модулем REST API платформы.

Java+JavaScript - альянс, рожденный в интернете

Всего лишь восемь лет назад в мире Java-разработчиков язык JavaScript воспринимался как некий “бедный родственник”, пригодный разве что для добавления некоторой динамики в веб-страницы. Основная же разметка генерировалась на сервере Java фреймворками, такими как JSF, Struts, Tapestry или Thymeleaf. А сейчас на наших глазах JavaScript стал языком номер один не только для разработки клиентского интерфейса, благодаря React, Vue, Angular и другим, но с появлением Node.js JavaScript уверенно занял свое место и в разработке бэкенда.

В реальности, в веб-приложениях обычно используется больше одного языка программирования: JavaScript для клиентской части, Java для бизнес-логики, SQL для выборки данных, Python для анализа и т.д. И нам как разработчикам приходится комбинировать эти языки в рамках одного приложения разными способами. Самый распространенный случай - REST API. Основанный на платформо-независимом протоколе HTTP и простом формате данных JSON, теперь это самый распространенный способ соединения двух языков: JavaScript на клиенте и Java на сервере.

Но даже самый незаметный шов все равно остается швом. При определении API вечно приходится решать одни и те же вопросы: какой метод вызывать, какая там модель данных, передавать, например, адрес клиента одной строкой или все-таки в структурированном виде, и прочее, и прочее.

Как же нам помочь друг другу, ускорить написание кода и избежать потерь времени на непродуктивные обсуждения и ошибки при коммуникациях?

Swagger - ответ на все?

Вы скажете: "Swagger" и будете правы. Swagger - это де-факто индустриальный стандарт для проектирования, создания, документирования и потребления REST API. Существует большое количество генераторов кода для того, чтобы сделать SDK и клиентские библиотеки на разных языках программирования.

Фреймворк CUBA поддерживает генерацию описания API в формате Swagger. Как только вы добавляете модуль REST API к приложению, у вас появляется вызов API, который возвращает полное описание существующих сервисов в формате .json или .yaml. И можно использовать эти описания для того, чтобы сгенерировать клиентский код на JS.

Но нужно отметить, что Swagger - это всего лишь инструмент для описания API. А вот какую информацию хочет видеть разработчик клиента? “Классический” ответ - определите все бизнес-функции, которые должно выполнять приложение, и создайте хороший API. Потом опубликуйте его в виде REST сервисов, добавьте документацию Swagger и наслаждайтесь результатами.

Но тогда почему GraphQL бьет все рекорды по приросту использования на стороне фронт-энда? И его доля среди разных видов Web API продолжает расти? Что происходит? Оказывается, что зачастую гораздо проще выдать разработчикам фронт-энда некий обобщенный интерфейс, чтобы не делать множество методов получения данных, требования к которым могут, к тому же, ещё и часто меняться вместе с изменениями дизайна клиентской части. Например, для оформления заказа сначала может потребоваться просто список товаров с ценами. Потом - с ценами, налогом и финальной ценой. Потом - с разбивкой товаров по категориям и суммами для каждой категории. GraphQL решает такие задачи играючи. Вдобавок ко всему, мы избегаем выборки слишком большого или слишком малого объема данных, равно как и опроса нескольких API для выборки данных, чтобы собрать вместе сложные структуры.

Хорошо, похоже, что есть смысл публиковать не только бизнес-функции, но ещё и некоторый API общего назначения. Это позволит разработчикам фронт-энда вызывать сложные функции одним запросом, но, в то же время, дает им некоторую гибкость в выборе способов работы с данными, избавляя разработчиков бэк-энда от необходимости реагировать на любое изменение в дизайне пользовательского интерфейса.

И есть ещё одна проблема, которую не решают ни Swagger, ни GraphQL, ни OData: что нужно делать со сгенерированным кодом клиентской части, если в API произошли изменения. Простая однонаправленная кодогенерация - это легко, а вот поддержка - совсем другое дело. Как можно быть уверенным, что наше клиентское приложение не сломается, если мы удалим какое-нибудь свойство у сущности в модели данных?

Итак, чтобы ускорить разработку фронт-энда и упростить взаимодействие между командами разработчиков, нам нужно несколько вещей:

  1. Публиковать не только бизнес-функции, но и API общего назначения
  2. Генерировать клиентский код, основываясь на модели данных сервера и сигнатурах методов сервисов
  3. Модифицировать сгенерированный код с минимумом проблем и потенциальных ошибок

В случае фреймворка CUBA ответом может послужить модуль REST API и генератор SDK для фронт-энда.

CUBA TypeScript SDK

В CUBA, модуль REST API предоставляет следующую функциональность:

  • CRUD операции на уровне модели данных
  • Исполнение предварительно заданных запросов JPQL
  • Вызов методов сервисов
  • Получение метаданных (сущности, представления, перечисления, типы данных)
  • Получение привилегий текущего пользователя (доступ к сущностям, атрибутам, особые привилегии)
  • Получение информации о текущем пользователе (имя, язык интерфейса, временная зона и т.д.)
  • Работа с файлами

Итак, у нас есть все, чтобы работать с серверной частью из любого клиента. Все вышеперечисленные API описаны в swagger файле YAML или JSON, так что можно начинать разработку сразу.

Хочется особо отметить, что очень важно установить правила доступа к REST API CUBA приложения, чтобы предотвратить случайный доступ к довольно мощным встроенным функциям фреймворка. Перво-наперво закройте доступ всем пользователям приложения к REST API, а потом выдавайте привилегии только нужным ролям.

Но CUBA предоставляет больше, чем просто встроенный REST API. Вы можете сгенерировать SDK, который может быть использован в качестве основы для разработки клиента с использованием практически для любого фронт-энд фреймворка: React, Angular, Vue или какого-либо ещё.

Этот генератор создает набор классов на языке TypeScript, при помощи которых можно вызывать функции CUBA API из клиентского приложения.

Чтобы сгенерировать SDK для CUBA приложения с подключенным REST API модулем, нужно запустить

npm install -g @cuba-platform/front-generator

а затем

gen-cuba-front sdk:all

и все необходимые классы будут созданы. Есть возможность сгенерировать простой, но полнофункциональный пользовательский интерфейс на ReactJS, чтобы пользователи начали вносить данные в ваше приложение как можно раньше. Конечно, при помощи сгенерированного интерфейса не получится выполнить какие-то сложные операции, но все-таки это будет полноценное приложение с авторизацией, ролевым доступом к данным и т.д.

Давайте посмотрим, что же предоставляет нам SDK.

Модель данных

Модель данных приложения представлена набором TypeScript классов. Если посмотреть на "Планировщик Докладов", который создается в учебнике "Быстрый Старт", то там есть такая сущность:

@NamePattern("%s %s|firstName,lastName")
@Table(name = "SESSIONPLANNER_SPEAKER")
@Entity(name = "sessionplanner_Speaker")
public class Speaker extends StandardEntity {
   @NotNull
   @Column(name = "FIRST_NAME", nullable = false)
   protected String firstName;

   @Column(name = "LAST_NAME")
   protected String lastName;

   @Email
   @NotNull
   @Column(name = "EMAIL", nullable = false, unique = true)
   protected String email;
//Setters and getters here
}

А в SDK у нас будет соответствующий класс:

export class Speaker extends StandardEntity {
   static NAME = "sessionplanner_Speaker";
   firstName?: string | null;
   lastName?: string | null;
   email?: string | null;
}

Более того, все связки с другими сущностями также будут перенесены в эту модель, так что вы сможете делать выборку целых структур данных, вместо того, чтобы делать несколько запросов к отдельным API.

Больше никаких DTO - вы получаете ровно такую же модель, какая определена на сервере.

Бизнес-сервисы

Для всех сервисов, которые опубликованы через REST API в CUBA, генерируется их представление в SDK. Например, если мы опубликуем сервис Session Service, то соответствующий ему код будет примерно таким:

export var restServices = {
   sessionplanner_SessionService: {
       rescheduleSession: (cubaApp: CubaApp, fetchOpts?: FetchOptions) => (params: sessionplanner_SessionService_rescheduleSession_params) => {
           return cubaApp.invokeService("sessionplanner_SessionService", "rescheduleSession", params, fetchOpts);
       }
   }
};

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

restServices.sessionplanner_SessionService.rescheduleSession(cubaREST)({session, newStartDate}).then( (result) => {
   //Result handling
});

Удобно, не так ли? Всю рутину берет на себя SDK.

API общего назначения

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

loadEntities<T>(entityName: string, options?: EntitiesLoadOptions, fetchOptions?: FetchOptions): Promise<Array<SerializedEntity<T>>>;
deleteEntity(entityName: string, id: any, fetchOptions?: FetchOptions): Promise<void>;

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

cubaREST.loadEntities<Speaker>(Speaker.NAME).then( (result => {
 //Result handling
}));

Используя этот API, разработчик может сделать свое собственное серверное приложение на JavaScript, которое будет публиковать свой собственный набор функций, и развернуть его на Node.js сервере. Этот архитектурный шаблон называется “backend for frontend” и применяется для создания функциональности, которая нужна только в клиентских приложениях. Более того, можно опубликовать несколько наборов собственных API для разных клиентских технологий: ReactJS, iOS и т.д. Генерируемый SDK идеально подходит для решения такой задачи благодаря большому количеству низкоуровневых функций.

Что не очень здорово в случае API общего назначения, так как это шанс выборки ненужных атрибутов или, наоборот, недостаточное количество данных, выбираемых за один запрос. Механизм Entity Views фреймворка CUBA решает эту проблему на стороне сервера, и SDK предоставляет такую же возможность для клиента. Для каждого класса сущности мы генерируем ещё и следующий код:

export type SpeakerViewName = "_minimal" | "_local" | "_base";

export type SpeakerView<V extends SpeakerViewName> = 
V extends "_minimal" ? Pick<Speaker, "id" | "firstName" | "lastName"> : 
V extends "_local" ? Pick<Speaker, "id" | "firstName" | "lastName" | "email"> : 
V extends "_base" ? Pick<Speaker, "id" | "firstName" | "lastName" | "email"> : 
never;

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

text

Обновления API

Как уже упоминалось выше, генерация кода - только половина дела. Поддержка изменений - вот что занимает большую часть времени разработчика. Генератор SDK анализирует изменения в коде при каждом следующем запуске и меняет только те файлы, которые необходимо. А компилятор TypeScript постарается сделать так, чтобы вы не забыли внести изменения и в свой собственный код, написанный с использованием SDK. Конечно, если вы используете TypeScript при разработке.

Заключение

Если вам нужно разработать клиентское приложение на JavaScript с использованием React/React Native или Angular или Vue в дополнение к стандартному интерфейсу, который предлагает фреймворк CUBA, то вы можете использовать модуль REST API в сочетании с генератором TypeScript SDK. Какую бы технологию вы не выбрали, вы сможете сосредоточиться на дизайне интерфейса, производительности или потреблении памяти вместо того, чтобы делать рутинные задачи. И можно быть уверенным, что коммуникации между клиентом и сервером, а также поддержка изменений в API будет наименьшей из всех проблем, с которыми вы столкнетесь.

Андрей Беляев