ym88659208ym87991671
Пример использования DataSpace с функцией на JavaScript | Документация для разработчиков
Skip to main content

Пример использования DataSpace с функцией на JavaScript

Вступление

В данной инструкции речь пойдет о быстрой разработке приложения (микросервисном подходе). Основная идея заключается в том, чтобы по итогам прочтения можно было бы начать быстро и удобно писать полноценные микросервисы (front + back + хранилище данных). Рассматриваемый пример будет написан на TypeScript/JavaScript, но это требование не является обязательным. Код функций может быть также написан на Java или Python. Обращение к DataSpace через GraphQL не накладывает каких-либо ограничений на клиента.

В инструкции мы познакомимся с продуктом Platform V DataSpace, напишем frontend-приложение, используя DataSpace как сервис (Backend-as-a-Service).

Работа в Studio

Для того, чтобы начать работу в Studio:

  • Зайдите на сайт https://developers.sber.ru/studio/login.
  • Зарегистрируйтесь или войдите по Сбер ID (можно через QR-код в приложении СберБанк Онлайн).
  • Создайте «Личное пространство».
  • В созданном пространстве нажмите Создать проект, где выберите Platform V DataSpace.
  • После создания проекта вы попадете в визуальный редактор конструирования модели данных.

Далее в разделе Знакомство с DataSpace описано, как формировать модель данных предметной области вашего приложения, выпускать соответствующий сервис.

Можно сразу же загрузить готовую модель данных для приложения «Промоакция»: model.xml и нажать кнопку Выпустить. После выпуска сервиса по данному адресу <https://smapi.pv-api.sbc.space/fn_d2527eab_2999_4a9a_99b1_4f90bf815b54> можно начать работу в приложении в роли администратора, где в качестве хранилища выступит ранее выпущенный вами сервис DataSpace. Нажав кнопку Login page, необходимо указать данные авторизации. Адрес, логин и пароль будут доступны в настройках вашего проекта DataSpace: адрес проекта, app_key, app_secret соответственно.

Приложение «Промоакция»

Для наглядности примера использования платформенных сервисов разработаем небольшое приложение «Промоакция».

Схема сервиса:

Видим два типа пользователей системы: «Администратор» и «Клиент». Для разработки сервиса воспользуемся сервисом Platform V DataSpace, уже доступным разработчику в Studio).

DataSpace

Знакомство с DataSpace

Для работы с данными мы будем использовать платформенный сервис DataSpace.

Материалы по этапам:

  1. Проектирование модели предметной области приложения в визуальном редакторе:
  2. Работа с данными через GraphQL API после выпуска сервиса DataSpace, в том числе через конструктор в визуальном редакторе: документация.

Подписание запросов (авторизация)

Итак, ранее мы уже создали модель предметной области и выпустили соответствующий сервис DataSpace, предоставляющий GraphQL API для работы с данными. Но для вызова извне необходимо отправлять HTTP-запросы на соответствующий сервис, предварительно подписывая их при помощи ключа.

Можно делать такие запросы, воспользовавшись соответствующим JavaScript SDK.

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

Данные для подписи запроса доступны в настройках соответствующего проекта в Studio:

В настройках доступны ak/sk, а также адрес сервиса для вызова:

Архитектура приложения «Промоакция»

У разных типов пользователей разные каналы для работы:

  • «Администратор» (Admin) работает с DataSpace через авторизованные запросы на API Gateway, зная адрес сервиса, appKey (логин) и appSecret (пароль), далее ak/sk. Необходимая для работы статика появляется посредством функции Function 1 Admin Frontend.
  • «Клиент» (Client) через общедоступный сервис вводит промокод и выбирает подарок. Воспользоваться промокодом можно только один раз. Необходимая для работы статика появляется посредством функции Function 2 Client Frontend. В функции Function 3 Client Backend реализуем серверную логику обработки запросов от «Клиентов».

Function 1 Admin Frontend

Перейдем к написанию frontend-приложения, когда в качестве backend используется DataSpace.

Необходимо иметь следующий технологический стек:

  • TypeScript — язык программирования.
  • GraphQL — язык запросов к серверной части (DataSpace).
  • GraphQL Code Generator (TypeScript) — очень полезная утилита для преобразования GraphQL-запросов в конструкции на TypeScript.
  • ReactJS — ReactJS.
  • Apollo Client v3 — JS-библиотека для работы через GraphQL: кэширование, react-хуки и другое.
  • Ant Design — готовые экранные компоненты.
  • Webpack 5 — средство для сборки.

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

Исходные ресурсы приложения доступны по ссылке на GitHub: https://github.com/VictorBiryukov/promo-action.git.

Разработка

Начнем с режима разработки: webpack.dev.config.js.

Наиболее интересно в конфигурации сборки — это настройки прокси у сервера разработки:

...
devServer: {
hot: true,
port: 3000,
before: (app) => {
app.use(createProxyMiddleware("/graphql",
{
target: process.env.DS_ENDPOINT,
changeOrigin: true,
secure: false,
pathRewrite: { '/graphql': '' }
}));
},
watchOptions: {
poll: true,
ignored: "/node_modules/"
}
},
...

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

Для корректной работы прокси-сервера необходимо в файле .env указать адрес вашего сервиса DataSpace:

DS_ENDPOINT=[Enter your dataspace graphql endpoint here]

Перейдем непосредственно к написанию приложения. В файле src/index.tsx необходимо подключить React, указать корневой компонент App.

Прежде чем начать работать с DataSpace, необходимо научить наш провайдер GraphQL-запросов правильно их подписывать. Для этого необходимо дать возможность пользователю ввести адрес DataSpace + ak/sk и сохранить данные в localStorage:

  <Form>
<Form.Item>
<Input placeholder="Service address"
value = {appAddress}
onChange={e => setAppAddress(e.target.value)}
/>
</Form.Item>
<Form.Item>
<Input placeholder="Service key"
value = {appKey}
onChange={e => setAppKey(e.target.value)}
/>
</Form.Item>
<Form.Item>
<Input.Password placeholder="Service secret"
value = {appSecret}
onChange={e => setAppSecret(e.target.value)}
/>
</Form.Item>
</Form>

Заполнив параметры, далее необходимо передать их в AppProvider, где уже при помощи ранее представленного JavaScript SDK определить правило подписания, после чего инициализировать ApolloClient:

  const authFetch = (uri: any, options: any) => {

let sig = new signer.Signer()
sig.Key = appKey
sig.Secret = appSecret
let request = new signer.HttpRequest(options.method, appAddress, options.headers, options.body)
sig.Sign(request)

return fetch(uri, options);
};

console.log(process.env.NODE_ENV);



return new ApolloClient({
cache: cache,
link: new HttpLink({
uri: process.env.NODE_ENV === 'production' ? appAddress : 'graphql',
fetch: authFetch,
})
})

Apollo Client призван значительным образом упростить работу с сервисом DataSpace через GraphQL API:

С одной стороны он хорошо интегрируется с React через хуки (hooks), с другой — обеспечивает бесшовную интеграцию с GraphQL-сервером DataSpace, решая вопросы кэширования и нормализации данных на стороне клиента.

Вернемся к приложению «Промоакция». В UI-консоли администратора нам необходимо обеспечить следующие возможности:

  • запрос списка компаний-спонсоров;
  • создание/удаление компании-спонсора;
  • запрос списка подарков компании-спонсора;
  • создание/удаление подарка.

Для написания соответствующих запросов воспользуемся GraphQL-конструктором внутри визуального редактора. В примере ниже мы одним запросом создаем компанию-спонсора (GiftVendor) и два ее первых подарка (Gift):

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

Уделим внимание двум моментам:

  • Указывая ключ SberBankAndTwoFirstGifts в параметре idempotencePacketId в мутации packet, мы делаем этот запрос идемпотентным.

    При повторном выполнении данного запроса (если первый был успешным) DataSpace вернет результат выполнения, но не будет повторять саму операцию создания (изменения состояния БД).

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

  • С помощью лексемы ref:createGiftVendor, передаваемой в поле vendor создаваемых подарков, обеспечена связь между подарками и компанией-спонсором, создаваемой на первом шаге выполнения пакета.

Детальное описание формата GraphQL-запросов DataSpace доступно в документации documentation/graphql.

Для нашего приложения понадобится набор простых GraphQL-запросов:

query searchGiftVendor{
searchGiftVendor{
elems{
id
__typename
name
}
}
}
mutation createGiftVendor($name:String!){
packet{
createGiftVendor(input:{
name: $name
}){
id
__typename
name
}
}
}
mutation deleteGiftVendor($id: ID!){
packet{
deleteGiftVendor(id: $id)
}
}
query searchGift($cond: String){
searchGift(cond: $cond){
elems{
id
__typename
serialNumber
kind
}
}
}
mutation createGift($vendorId: ID!, $serialNumber:String!, $kind: _EN_GiftKind){
packet{
createGift(input:{
vendor: $vendorId
serialNumber: $serialNumber
kind: $kind
}){
id
__typename
serialNumber
kind
}
}
}
mutation deleteGift($id: ID!){
packet{
deleteGift(id: $id)
}
}

Необходимо зафиксировать данные запросы в файле src/graphql/requests.graphql. Также понадобится GraphQL-схема API DataSpace, которая доступна в конструкторе визуального редактора. Справа в закладке SCHEMA выбираем DOWNLOADSDL:

Выгруженный файл необходимо поместить в корневую папку проекта schema.graphql.

Теперь необходимо осуществить генерацию Typescript-конструкций на основе запросов и схемы, зафиксированных ранее. В файле package.json подключены необходимые JS-библиотеки для этапа разработки и прописана команда генерации в разделе scripts:

...
"devDependencies": {
...
"@graphql-codegen/cli": "^1.9.0",
"@graphql-codegen/typescript": "1.22.3",
"@graphql-codegen/typescript-operations": "1.18.2",
"@graphql-codegen/typescript-react-apollo": "2.2.7",
...
},
...
"scripts": {
...
"codegen": "graphql-codegen --config codegen.yml",
...
},
...

Необходимо сконфигурировать правила генерации в codegen.yml:

overwrite: true # флаг перезаписи файла генерируемого кода
schema: 'schema.graphql' # файл graphql-схемы
documents: 'src/graphql/**/*.graphql' # маска файлов c graphql-запросами
generates:
./src/__generate/graphql-frontend.ts: # результирующий файл генерации
plugins:
- typescript # генерация типов
- typescript-operations # генерация операций
- typescript-react-apollo # генерация React Apollo компонентов

Все готово для генерации. Запускаем из консоли соответствующую команду npm run codegen:

Генерация прошла успешно, результаты:src/__generate/graphql-frontend.ts.

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

  • aggVersion: версия агрегата, которую можно использовать для формирования транзакции между получением и сохранением данных в БД (оптимистическая блокировка).
  • lastChangeDate: дата/время последнего изменения экземпляра сущности.
  • type: тип сущности (может быть полезен в случае использования наследования в модели данных).
  • aggregateRoot: ID корня агрегата.

Например, тип для сущности Gift:

export type Gift = {
id: Scalars['ID'];
aggVersion: Scalars['Long'];
lastChangeDate?: Maybe<Scalars['_DateTime']>;
chgCnt?: Maybe<Scalars['Long']>;
kind?: Maybe<_En_GiftKind>;
serialNumber: Scalars['String'];
type: Scalars['String'];
vendor: GiftVendor;
aggregateRoot?: Maybe<GiftVendor>;
...
};

Также отражено определенное ранее перечисление GiftKind:

export enum _En_GiftKind {
Cap = 'CAP',
Tshirt = 'TSHIRT',
Mug = 'MUG'
}

В дальнейшем воспользуемся данными перечислением при написании формы создания подарков.

В конце файла имеется сгенерированный ряд хуков, соответствующих GraphQL-запросам (src/graphql/requests.graphql). Например, запросы searchGift, createGift, deleteGift представляют следующие функции:

...
export function useSearchGiftQuery(baseOptions?: Apollo.QueryHookOptions<SearchGiftQuery, SearchGiftQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SearchGiftQuery, SearchGiftQueryVariables>(SearchGiftDocument, options);
}
...
export function useCreateGiftMutation(baseOptions?: Apollo.MutationHookOptions<CreateGiftMutation, CreateGiftMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateGiftMutation, CreateGiftMutationVariables>(CreateGiftDocument, options);
}
...
export function useDeleteGiftMutation(baseOptions?: Apollo.MutationHookOptions<DeleteGiftMutation, DeleteGiftMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteGiftMutation, DeleteGiftMutationVariables>(DeleteGiftDocument, options);
}
...

Это функции-обертки над React-хуками Apollo.useQuery и Apollo.useMutation. Данные конструкции призваны типизировать бесшовную интеграцию между серверной и клиентской частью приложения. Взглянем более детально на useCreateGiftMutation:

  • CreateGiftMutationVariables определяют сигнатуру входящих параметров:
...
export type CreateGiftMutationVariables = Exact<{
vendorId: Scalars['ID'];
serialNumber: Scalars['String'];
kind?: Maybe<_En_GiftKind>;
}>;
...
  • CreateGiftMutation определяет сигнатуру возвращаемого результата:
...
export type CreateGiftMutation = (
{ __typename?: '_Mutation' }
& { packet?: Maybe<(
{ __typename?: '_Packet' }
& { createGift?: Maybe<(
{ __typename: '_E_Gift' }
& Pick<_E_Gift, 'id' | 'serialNumber' | 'kind'>
)> }
)> }
);
...

По итогам первого этапа:

  • научились подписывать HTTP-запросы к серверной части;
  • определили набор GraphQL-запросов, которые нам понадобятся для работы;
  • осуществили генерацию необходимых Typescript-конструкций, призванных упростить работу при написании кода.

Перейдем непосредственно к прикладным формам. Форма отображения/добавления/удаления компаний-спонсоров реализована в соответствующем компоненте src/components/GiftVendorList.tsx. Она отражает список доступных компаний в виде вкладок (Tabs):

Кнопка Add new gift vendor позволяет заводить новые компании-спонсоры:

Кнопка Delete gift vendor удаляет компанию.

Необходимо обратить внимание в коде на следующие моменты:

  • Для получения списка компаний-спонсоров используется хук useSearchGiftVendorQuery, который был сгенерирован на основе запроса SearchGiftVendor, зафиксированного в файле src/graphql/requests.graphql.
  • Из результата выполнения хука деструктуризируем параметры data, loading, error далее обрабатываем соответствующим образом значения loading, error.
...
const { data, loading, error } = useSearchGiftVendorQuery()
const giftVendorList = data?.searchGiftVendor.elems
...
if (loading) return (<Spin tip="Loading..." />);
if (error) return <p>`Error! ${error.message}`</p>;

Сам GraphQL-запрос SearchGiftVendor возвращает JSON-структуру следующего вида:

Так как в дальнейшем для отрисовки вкладок с компаниями нужен только сам массив компаний, определим для этого отдельную константу giftVendorList, ссылающуюся на массив elems возвращаемой запросом конструкции. Преимущество такого подхода заключается в том, что осуществляется работа с данными через типизированную структуру, которая основывается на модели предметной области приложения, описанной ранее в DataSpace. Более подробно про работу с мутациями в Apollo можно ознакомиться в статье. Далее при помощи функции getTabs, принимающей на вход параметром список полученных компаний, необходимо создать вкладки:

...
<Form style={{ margin: "10px" }}>
<Form.Item>
<Tabs>
{getTabs(giftVendorList)}
</Tabs>
</Form.Item>
</Form>
...

Далее перейдем к добавлению/удалению компаний:

...
const [createGiftVendorMutation] = useCreateGiftVendorMutation()
const [deleteGiftVendorMutation] = useDeleteGiftVendorMutation()
...

Необходимо вытащить из соответствующих хуков функции — мутации добавления/удаления. Более подробно про работу с мутациями в Apollo можно почитать в статье.

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

  • variables— это набор параметров, заполняемых пользователем на форме. В нашем случае — имя компании-спонсора.
  • update — параметр позволяет передать функцию, которая в нашем случае обновит кэш Apollo-клиента (store) для запроса SearchGiftVendor, добавив туда результат (result) GraphQL-запроса createGiftVendor.
...
<Modal visible={showCreateForm}
onCancel={() => setShowCreateForm(false)}
onOk={() => {
setShowCreateForm(false)
createGiftVendorMutation({
variables: {
name: vendorName!
},
update: (store, result) => {
store.writeQuery({
query: SearchGiftVendorDocument,
data: {
searchGiftVendor: {
elems: [, ...giftVendorList!, result.data?.packet?.createGiftVendor]
}
}
})
}
})
}}
>
...

Такой подход позволяет обновлять списочные формы (в нашем случае — список вкладок), не делая дополнительного запроса на сервер. Более детально об этом механизме можно ознакомиться в статье.

С удалением ситуация аналогична, только здесь обработчик не добавляет элемент, а фильтрует массив, исключая ранее удаленный элемент:

...
<Button style={{ margin: "20px" }}
key={elem.id ?? ""}
onClick={(e) => {
deleteGiftVendorMutation({
variables: {
id: elem.id
},
update: (store) => {
store.writeQuery({
query: SearchGiftVendorDocument,
data: {
searchGiftVendor: {
elems: giftVendorList!.filter(x => x.id !== elem.id)
}
}
})
}
})
}}>Delete gift vendor</Button>cond: "it.vendor.$id == '" + vendorId + "'"
...

Функциональность для заведения подарков в рамках конкретной компании-спонсора аналогична заведению самих компаний src/components/GiftList.tsx. Вместо компонента Tabs используется компонент Table в самом простом его варианте.

Необходимо обратить внимание на два момента:

  • В хук useSearchGiftQuery передается через переменную cond соответствующего GraphQL-запроса условие фильтрации: "it.vendor.$id == '" + vendorId + "'". То есть запрашиваются подарки только конкретной компании-спонсора, на вкладке которой мы находимся. При формировании условий используется нативно понятный язык регулярных выражений, оперирующий структурой модели. В то же время доступен гибкий и мощный инструментарий составления различного рода условий выборки данных. Например, запрос всех спонсоров, начинающихся с лексемы 'Sber' и имеющих хотя бы один подарок:
query searchGiftVendor{
searchGiftVendor(cond: "it.name $like 'Sber%' && it.gifts.$exists"){
elems{
name
lastChangeDate
gifts(cond:"it.lastChangeDate < root.lastChangeDate.$addDays(1) || it.serialNumber.$substr(1,1) == '1'"){
elems{

serialNumber
}
}
}
}
}

У таких компаний интересны только подарки, которые создавались/менялись в течение суток после создания компании-родителя. Детальное описание всех возможностей данного синтаксиса фильтрации приведено в статье.

  • Имеются возможности сортировки и постраничной вычитки запросов. С деталями можно ознакомиться в статье.

Итак, имеется приложение для фиксации компаний-спонсоров и выпускаемых ими подарков.

Если попробовать аналогичным образом добавить формы для фиксации серий (промоакций) и выдаваемых в рамках акций ваучеров (промокодов), то можно увидеть, что это не займет много времени.Необходимые сущности в модели и сервис для работы с ними уже имеется. Необходимо лишь зафиксировать новые запросы по аналогии с запросами к спонсорам и их подаркам: src/graphql/requests.graphql. А также отразить новые формы по аналогии с ранее рассмотренными компонентами: src/components/GiftVendorList.tsx и src/components/GiftList.tsx.

Обновлено 19 сентября 2022

Заметили ошибку?

Выделите текст и нажмите Ctrl + Enter, чтобы сообщить нам о ней