# Урок 25. Создание API-запросов

В этом уроке мы разберем, как создавать кастомные API-запросы, которые позволят сторонним сервисам взаимодействовать с нашим сервером.

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](https://wfsys.gitbook.io/wt-practice/customization/lesson_scheduler#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](https://wfsys.gitbook.io/wt-practice/main/lesson_list_form#podklyuchenie-shablonov-k-proektu).
{% endhint %}

{% hint style="info" %}
Для тестирования API-запросов в уроке используется приложение [Postman](https://www.postman.com/), которое можно скачать с официального сайта.
{% endhint %}

## Создание пользователя

Все кастомные API-запросы, которые отправляются на сервер из мобильных приложений или с сайта, должны быть подписаны токеном, который создается при авторизации пользователя в программе.

Для начала добавим специального пользователя "Пользователь для сайта", под которым будет авторизовываться сторонний ресурс для доступа к API:

```sql
DO
$BODY$
DECLARE
  _permission_block_id smallint;
  _permission_block_item_id smallint;
  _permission_id smallint;

  _group_id smallint;
  _public_user_id smallint;
  _user_id smallint;
  
BEGIN

  -- Добавление разрешений
  INSERT INTO template.permission_block(id_title, title)
  VALUES ('site', 'Сайт')
  ON CONFLICT (id_title) DO NOTHING
  RETURNING permission_block_id INTO _permission_block_id;

  INSERT INTO template.permission_block_item(permission_block_id, id_title, title)
  VALUES (_permission_block_id, 'site_api', 'API-запросы для сайта')
  ON CONFLICT (id_title) DO NOTHING
  RETURNING permission_block_item_id INTO _permission_block_item_id;

  INSERT INTO template.permission(name, permission_block_item_id)
  VALUES ('SiteApiMethodPermission', _permission_block_item_id)
  ON CONFLICT (name) DO NOTHING
  RETURNING permission_id INTO _permission_id;

  INSERT INTO public.strings (strings_id, language_id, text_value)
  VALUES
    ('site', 1, 'Сайт'),
    ('site', 2, 'Site'),
    ('site_api', 1, 'API-запросы для сайта'),
    ('site_api', 2, 'Website API requests');
  -- Добавление разрешений

  -- Создание группы
  INSERT INTO template.group (name, title, description)
  VALUES ('UserForSiteGroup', 'Пользователи для сайта', 'Группа пользователей для работы с сайтом')
  RETURNING group_id INTO _group_id;
  
  INSERT INTO template.group_permission(group_id, permission_id)
  VALUES (_group_id, _permission_id)
  ON CONFLICT (group_id, permission_id) DO NOTHING;
  -- Создание группы

  -- Создание пользователя
  CREATE EXTENSION pgcrypto;
  
  INSERT INTO public.user(user_name, user_full_name, user_password, person)
  VALUES('USER_FOR_SITE', 'Пользователь для сайта', encode(digest('123', 'sha512'), 'hex'), false)
  RETURNING user_id INTO _public_user_id;

  INSERT INTO template.user(public_user_id)
  VALUES(_public_user_id)
  RETURNING user_id INTO _user_id;

  INSERT INTO template.user_group(user_id, group_id)
  VALUES(_user_id, _group_id);

  DROP EXTENSION pgcrypto;
  -- Создание пользователя

END;
$BODY$;
```

Для пользователя создали группу, которой сразу назначили права доступа до **SiteApiMethodPermission**. Позже создадим это разрешение в серверном xml-файле.

Из запроса на получение списка групп пользователей необходимо исключить группу "Пользователи для сайта", чтобы нельзя было редактировать ее и выбирать при создании/редактировании пользователей.

Необходимо исключить блок "Сайт" из списка редактирования прав доступа для групп пользователей (PermissionBlockItemSelectSqlQuery).

## Аутентификация

### Запрос аутентификации

Для авторизации и получения токена используется POST-запрос /api/v1/tokens/signin.

Давайте создадим такой запрос в Postman:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FWUIrO1wx4IS3ghu3oDEp%2Fimage.png?alt=media&#x26;token=a8ba02f9-d453-4860-8bfb-c3c08a91f4e8" alt=""><figcaption></figcaption></figure>

Где localhost:49707 - IP-адрес и порт сервера.

Тело запроса:

```json
{
    "UserName":"USER_FOR_SITE",
    "PasswordHash":"3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1eb8b85103e3be7ba613b31bb5c9c36214dc9f14a42fd7a2fdb84856bca5c44c2",
    "LongToken":true
}
```

В теле запроса указаны три поля:

* UserName - логин пользователя. Обязательное поле. Ожидается строковое значение;
* PasswordHash - хэш пароля (Sha512). Обязательное поле. Ожидается строковое значение;
* LongToken -  признак использования токена с большим временем жизни. Необязательное поле. Ожидается логическое значение. По умолчанию используется значение False.

Если LongToken имеет значение false, то время жизни токена равно 10 минутам. Если true, то 15 годам.

В ответ от сервера мы получим:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fl5ZOiY8eDeeLJKLRphaK%2Fimage.png?alt=media&#x26;token=721fe2e8-8b13-4e04-9842-6c00112d72a9" alt=""><figcaption></figcaption></figure>

В ответе поля:

* accessToken - основной токен доступа. Им необходимо подписывать все запросы;
* refreshToken - токен, необходимый для обновления основного токена;
* expires - срок жизни (точнее, срок годности) accessToken, указанный в секундах с 1970-01-01.&#x20;

### Запрос на обновление токена

Для обновления токена используется POST-запрос /api/v1/tokens/refresh:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FnqUg1MUvYIKYKT0xhy6H%2Fimage.png?alt=media&#x26;token=e9ca8d1a-31bd-42f2-bd00-84fdb976d950" alt=""><figcaption></figcaption></figure>

Тело запроса:

```json
{
    "Token":"c7e078ba9d6c031ade9520b1cae90ccb33a1b0c87d964e6ad648ae938239d04aba39ae44828927bb30e2e655ca4d72acf10e95ccf973601afdd2a3981d4b2584"
}
```

В поле Token указывается refreshToken, полученный в POST-запросе /api/v1/tokens/signin.

Ответ от сервера буде содержать те же поля, что и в запросе на аутентификацию:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FUR4MCtqjSRocZ5Z0vVB1%2Fimage.png?alt=media&#x26;token=75a8af2c-4018-417e-bb93-d1a259749271" alt=""><figcaption></figcaption></figure>

Отлично, теперь можем приступить к созданию кастомных API-запросов.

## API-запросы

Перейдем в серверный xml-файл (Template.xml) и перед тэгом `<Scheduler>` добавим тэг [`<ApiMethods>`](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/api_method), в котором будем описывать кастомные запросы.

{% hint style="info" %}
Шаблон маршрута для кастомных API-запросов имеет вид:

`http://localhost:49707/data_api/{route}`

где {route} - конечная точка маршрута.
{% endhint %}

### GET-запрос

Создадим API-запрос на получение списка единиц измерения:

{% code title="Template.xml" %}

```xml
    <ApiMethod Name="GetUnitListApiMethod" Route="unit_list" Method="Get" VersionCode="1">
      <Response>
        <Objects>
          <Object Name="units" Array="True">
            <Command Name="UnitListSelectSqlQueryCommand" />
          </Object>
        </Objects>
      </Response>
    </ApiMethod>
```

{% endcode %}

В атрибуте `Route` тэга `<ApiMethod>` указываем конечную точку маршрута, по которому будет выполняться действие, описанное в ApiMethod. В нашем случае полный маршрут будет иметь вид: <http://localhost:50707/data\\_api/unit\\_list>.

Необязательный тэг `<Response>` описывает дополнительные поля в ответе на запрос. Таким образом, помимо системного поля **result\_code**, будет добавлено поле **units**, содержащее массив объектов. За то, что поле будет возвращать массив, отвечает необязательный атрибут `Array` тэга `<Object>`. В качестве значения поля units будет подставляться результат выполнения команды **UnitListSelectSqlQueryCommand** типа SqlQueryCommand:

{% code title="Template.xml" %}

```xml
<Command Name="UnitListSelectSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      SELECT
        unit.unit_id,
        unit.title,
        short_title,
        unit.archive
      FROM
        template.unit
      ORDER BY unit.title;
    </Text>
  </SqlQuery>
</Command>
```

{% endcode %}

Добавим API-метод GetUnitListApiMethod в разрешение SiteApiMethodPermission, которое ранее добавили для группы "Пользователи для сайта":

{% code title="Template.xml" %}

```xml
<Permission Name="SiteApiMethodPermission" Type="ApiMethodPermission">
  <ApiMethods>
    <ApiMethod Name="GetUnitListApiMethod" />
  </ApiMethods>
</Permission>
```

{% endcode %}

Давайте перейдем в Postman и создадим запрос *localhost:49707/data\_api/unit\_list* для проверки метода:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fe63Lbo3pIISq90pKVkIG%2Fimage.png?alt=media&#x26;token=07066a67-8678-48cd-8dfc-2f34cc0384cd" alt=""><figcaption></figcaption></figure>

Все запросы должны быть подписаны полученным при аутентификации jwt-токеном, поэтому в заголовок запроса добавили параметр **Authorization** со значением: **Bearer \<accessToken>**.

В ответ на запрос сервер вернет json-объект:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FLc6FcElPf313wIjvkV94%2Fimage.png?alt=media&#x26;token=fddb55b2-c941-44c5-8b9c-7e71ea033110" alt=""><figcaption></figcaption></figure>

### GET-запрос с двумя объектами

Создадим команды для получения списков категорий ТМЦ и самих ТМЦ:

{% code title="Template.xml" %}

```xml
<Command Name="MaterialCategorySelectSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      SELECT
        MC.material_category_id,
        MC.parent_material_category_id,
        MC.title
      FROM
        template.material_category MC
      WHERE
        NOT MC.archive
      ORDER BY MC.title, MC.material_category_id;
    </Text>
  </SqlQuery>
</Command>

<Command Name="MaterialSelectSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      SELECT
        M.material_id,
        M.material_category_id,
        M.title,
        M.unit_id,
        M.unit_price
      FROM
        template.material M
      WHERE
        NOT M.archive
      ORDER BY M.title;
    </Text>
  </SqlQuery>
</Command>
```

{% endcode %}

Добавим API-метод для получения этих списков:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="GetMaterialListApiMethod" Route="material_list" Method="Get" VersionCode="1">
  <Response>
    <Objects>
      <Object Name="material_category" Array="True">
        <Command Name="MaterialCategorySelectSqlQueryCommand" />
      </Object>
      <Object Name="material" Array="True">
        <Command Name="MaterialSelectSqlQueryCommand" />
      </Object>
    </Objects>
  </Response>
</ApiMethod>
```

{% endcode %}

В Postman создадим и выполним запрос *localhost:49707/data\_api/material\_list*:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FTFebvyYkkVVo0SLXrxz4%2Fimage.png?alt=media&#x26;token=ec146ee0-f48c-46bc-8556-707f094a9fcb" alt=""><figcaption></figcaption></figure>

В ответ на запрос сервер вернет json-объект:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Flt2IlbQ1Z5qH8w4kImkU%2Fimage.png?alt=media&#x26;token=275eb4cf-a2d8-4682-a4e1-59a0133976e6" alt=""><figcaption></figcaption></figure>

В ответе мы видим два поля: **material\_category** и **material** с массивами объектов.

### GET-запрос с параметрами

Давайте создадим API-запрос, который по идентификатору клиента будет возвращать список его заказов.

Создадим команду:

{% code title="Template.xml" %}

```xml
<Command Name="OrdersByClientIdSelectSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      WITH _order AS (
        SELECT
          O.order_id,
          O.client_id,
          O.order_number,
          O.order_date,
          O.description
        FROM
          template.order O
        WHERE
          O.client_id = {client_id}
      ), _order_position AS (
        SELECT
          O.order_id,
          SUM(OP.quantity * OP.unit_price) AS total_cost
        FROM
          _order O
          JOIN template.order_position OP USING(order_id)
          LEFT JOIN template.material M USING(material_id)
        GROUP BY O.order_id
      )
      
      SELECT
          O.order_id,
          O.order_number,
          O.order_date,
          COALESCE(OP.total_cost, 0) AS total_cost
      FROM
        _order O
        LEFT JOIN _order_position OP USING(order_id)
      ORDER BY O.order_number ASC;
    </Text>
  </SqlQuery>
</Command>
```

{% endcode %}

В тексте запроса используется переменная client\_id. Вместо этой переменной будет подставляться значение параметра, полученного в HTTP-запросе.

Добавим ApiMethod:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="GetOrderListByClientIdApiMethod" Route="order_list" Method="Get" VersionCode="1">
  <Parameters>
    <Parameter Name="client_id" Type="Integer" Source="FromQuery" Required="True" />
  </Parameters>
  <Response>
    <Objects>
      <Object Name="orders" Array="True">
        <Command Name="OrderByClientIdSelectSqlQueryCommand" />
      </Object>
    </Objects>
  </Response>
</ApiMethod>
```

{% endcode %}

В тэге [`<Parameters>`](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/api_method#parameters) можем перечислять все входные параметры. Для каждого параметра необходимо указать его тип и источник. В нашем случае для GET-запроса будем ожидать параметр **client\_id** в строке запроса. Через атрибут `Required` укажем, что параметр является обязательным.

Выполним в Postman запрос *localhost:49707/data\_api/order\_list?client\_id=2*:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fbv7RcPQmQnbW7pCvBcDd%2Fimage.png?alt=media&#x26;token=fab8b67e-9072-4c3f-ad1c-6820a4aeb130" alt=""><figcaption></figcaption></figure>

В ответ на запрос сервер вернет json-объект:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fy4lWaYaeJG3CqjngAWfM%2Fimage.png?alt=media&#x26;token=1509ee85-c825-44db-9bb3-16b1756b2bb9" alt=""><figcaption></figcaption></figure>

Так как параметру client\_id указали источник FromQuery (значение атрибута Source), то контроллер будет искать этот параметр в строке запроса.

Давайте расширим этот запрос, добавив в него список позиций заказа:

{% code title="Template.xml" %}

```xml
<Command Name="OrderPositionsByClientIdSelectSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      WITH _order AS (
        SELECT
          O.order_id,
          O.client_id,
          O.order_number,
          O.order_date,
          O.description
        FROM
          template.order O
        WHERE
          O.client_id = {client_id}
      )
      SELECT
        O.order_id,
        OP.order_position_id,
        OP.material_id,
        OP.quantity,
        OP.unit_price
      FROM
        _order O
        JOIN template.order_position OP USING(order_id)
      ORDER BY OP.order_position_id;
    </Text>
  </SqlQuery>
</Command>
```

{% endcode %}

Мы можем просто добавить список позиций заказа отдельным полем:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="OrderListByClientIdApiMethod" Route="order_list" Method="Get" VersionCode="1">
  <Parameters>
    <Parameter Name="client_id" Type="Integer" Source="FromQuery" Required="True" />
  </Parameters>
  <Response>
    <Objects>
      <Object Name="orders" Array="True">
        <Command Name="OrdersByClientIdSelectSqlQueryCommand" />
      </Object>
      <Object Name="order_position" Array="True">
        <Command Name="OrderPositionsByClientIdSelectSqlQueryCommand" />
      </Object>
    </Objects>
  </Response>
</ApiMethod>
```

{% endcode %}

А можем, используя тэг [`<Relations>`](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/api_method#response_relations), описать отношения полей друг другу:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="OrderListByClientIdApiMethod" Route="order_list" Method="Get" VersionCode="1">
  <Parameters>
    <Parameter Name="client_id" Type="Integer" Source="FromQuery" Required="True" />
  </Parameters>
  <Response>
    <Objects>
      <Object Name="orders" Array="True">
        <Command Name="OrdersByClientIdSelectSqlQueryCommand" />
      </Object>
      <Object Name="order_position" Array="True">
        <Command Name="OrderPositionsByClientIdSelectSqlQueryCommand" />
      </Object>
    </Objects>
    <Relations>
      <Relation ParentObject="orders" ParentField="order_id" ChildObject="order_position" ChildField="order_id"></Relation>
    </Relations>
  </Response>
</ApiMethod>
```

{% endcode %}

В тэге `<Relation>` указываем имена родительского и дочернего объектов и их поля, по которым будет строиться дерево отношений.

Выполним тестовый запрос *localhost:49707/data\_api/order\_list?client\_id=2*:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FTdnFakKLYwR5pJFEVAH8%2Fimage.png?alt=media&#x26;token=d01e1095-ece6-4071-9ad2-cb00a63b495f" alt=""><figcaption></figcaption></figure>

В ответ на запрос сервер вернет json-объект:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Ftkg1M4N3U562PE3krgme%2Fimage.png?alt=media&#x26;token=c2cc77ff-428c-485b-bbe5-5e8a087f8ab7" alt=""><figcaption></figcaption></figure>

В ответе от сервера каждый объект заказа имеет массив позиций заказа.

Можно указывать несколько отношений. Добавьте команду на получение списка оплат в заказах для клиента и добавьте их в **OrderListByClientIdApiMethod**. Укажите в тэге `<Relation>` отношение списка оплат к списку заказов.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FlQyUbPBDMeE3ykHLslOm%2Fimage.png?alt=media&#x26;token=43887b1a-4c56-47bc-a65f-dd3321be3a36" alt=""><figcaption></figcaption></figure>

### POST-запрос

В качестве примера POST-запроса создадим API-метод на добавление оплаты в заказ.

Для начала добавим в таблицу **template.account** колонку for\_site для обозначения счета, на который будут привязываться оплаты с сайта, и добавим такой счет:

```sql
ALTER TABLE template.account
  ADD COLUMN for_site boolean NOT NULL DEFAULT false;
  
INSERT INTO template.account (title, cash, for_site)
VALUES ('Счет для оплат с сайта', false, true);
```

Создадим функцию для добавления оплаты через API-метод:

```sql
CREATE OR REPLACE FUNCTION template.order_payment_insert(
    IN in_order_id bigint,
    IN in_summ numeric)
  RETURNS TABLE(order_payment_id bigint, error_message character varying) AS
$BODY$
DECLARE
  _account_id bigint;
  _cash_id bigint;
  _order_payment_id bigint;
  _error_message varchar;
  _rec record;

BEGIN

  -- Проверка входящих параметров
  SELECT * INTO _rec FROM template.order WHERE order_id = in_order_id;
  _account_id = account_id FROM template.account WHERE for_site LIMIT 1;
  
  IF (_rec ISNULL) THEN
    _error_message = 'no_order';
  ELSIF (_rec.deleted) THEN
    _error_message = 'order deleted';
  ELSIF (_account_id ISNULL) THEN
    _error_message = 'no_account';
  END IF;

  IF (_error_message != '') THEN
    RETURN QUERY SELECT _order_payment_id, _error_message;
    RETURN;
  END IF;
  -- Проверка входящих параметров

  INSERT INTO template.cash(
    cash_date,
    account_id,
    operation_id,
    summ
  )
  SELECT
    now(),
    _account_id,
    O.operation_id,
    in_summ
  FROM
    template.operation O
  WHERE
    id_title = 'OrderPaymentOperation'
  RETURNING cash_id INTO _cash_id;

  INSERT INTO template.order_payment(
    order_id,
    cash_id
  )
  VALUES(
    in_order_id,
    _cash_id
  )
  RETURNING order_payment.order_payment_id INTO _order_payment_id;

  RETURN QUERY SELECT _order_payment_id, _error_message;

END;
$BODY$
  LANGUAGE plpgsql;
```

Функция будет возвращать идентификатор добавленной записи, либо ошибку, если передан неверный идентификатор заказа, заказ удален или отсутствует счет для оплат с сайта.

Создадим команду для вызова функции:

{% code title="Template.xml" %}

```xml
<Command Name="CreateOrderPaymentInsertSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      SELECT
        order_payment_id,
        error_message
      FROM
        template.order_payment_insert({order_id}::bigint, {summ}::numeric);
    </Text>
  </SqlQuery>
</Command>
```

{% endcode %}

Функция **template.order\_payment\_insert()** принимает два параметра, которые должны быть обязательными для запроса на добавления оплаты в заказ.

Создадим ApiMethod, который будет выполнять команду:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="AddOrderPaymentApiMethod" Route="add_order_payment" Method="Post" VersionCode="1">
  <Parameters>
    <Parameter Name="order_id" Type="Integer" Source="FromBody" Required="True" />
    <Parameter Name="summ" Type="Decimal" Source="FromBody" Required="True" />
  </Parameters>
  <Commands>
    <Command Name="CreateOrderPaymentInsertSqlQueryCommand" />
  </Commands>
  <Response>
    <Objects>
      <Object Name="order_payment_id">
        <Command Name="CreateOrderPaymentInsertSqlQueryCommand" Parameter="order_payment_id" />
      </Object>
      <Object Name="error_message">
        <Command Name="CreateOrderPaymentInsertSqlQueryCommand" Parameter="error_message" />
      </Object>
    </Objects>
  </Response>
</ApiMethod>
```

{% endcode %}

В тэге `<Commands>` можем описать последовательность команд, а в тэге `<Response>` обращаться к результату выполнения команд. При этом мы можем через атрибут Parameter обратиться к конкретному значению в результате команды.

Выполним тестовый запрос *localhost:49707/data\_api/add\_order\_payment*:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FjKawbZdlpnNfU9ECwJHC%2Fimage.png?alt=media&#x26;token=c7456dd1-f71b-4f3f-9782-4f0d13b67937" alt=""><figcaption></figcaption></figure>

В ответ на запрос сервер вернет json-объект:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FMNyJwKWpWqUnj1mzN2un%2Fimage.png?alt=media&#x26;token=91bc2448-5150-4840-96ca-1baeca3eb949" alt=""><figcaption></figcaption></figure>

### Кастомные типы параметров

Давайте создадим API-метод на сохранение заказа одним запросом. Т.е. помимо основной информации о заказе будем принимать и сохранять позиции заказа. У нас будет один метод на создание нового заказа и на редактирование существующего.

Первым делом в Postman создадим запрос *localhost:49707/data\_api/save\_order*:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fb5VKL4VB6FxqNZcD2fp8%2Fimage.png?alt=media&#x26;token=2f987fad-3853-486f-9f95-af5bd4d087d4" alt=""><figcaption></figcaption></figure>

Запрос будет передавать json-объект вида:

```json
{
   "client_id":8,
   "order":{
      "order_id":null,
      "order_date":"2023-12-21 11:00:28Z",
      "description":"Тестовый заказ с сайта",
      "order_position":[
         {
            "order_position_id":null,
            "material_id":4,
            "quantity":5,
            "unit_price":200
         }
      ]
   }
}
```

Для источника FromBody есть ограничение на обработку параметров относительно уровня вложенности объектов. В качестве параметров запроса можем указывать только поля основного объекта, которые принимают значение простых типов (строка, число, дата, логическое) и массив простых типов. Мы можем напрямую обратиться к полю client\_id и получить его значение, но не можем обратиться к полям объекта order. Поэтому в ApiMethod мы укажем параметр order с типом Json:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="SaveOrderApiMethod" Route="save_order" Method="Put" VersionCode="1">
  <Enabled>
    <Condition Name="SiteModuleIsOnSqlQueryCondition" />
  </Enabled>
  <Parameters>
    <Parameter Name="client_id" Type="Integer" Source="FromBody" Required="False" />
    <Parameter Name="order" Type="Json" Source="FromBody" Required="False" />
  </Parameters>
  <Commands>
    <Command Name="SaveOrderSqlQueryCommand" />
  </Commands>
  <Response>
    <Objects>
      <Object Name="order_id">
        <Command Name="SaveOrderSqlQueryCommand" Parameter="order_id" />
      </Object>
      <Object Name="order_number">
        <Command Name="SaveOrderSqlQueryCommand" Parameter="order_number" />
      </Object>
    </Objects>
  </Response>
</ApiMethod>
```

{% endcode %}

Следовательно, в команде SaveOrderSqlQueryCommand с параметром order будем работать как с json-строкой:

{% code title="Template.xml" %}

```xml
<Command Name="SaveOrderSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      SELECT
        order_id,
        order_number
      FROM
        template.save_order({client_id}::bigint, {order}::json);
    </Text>
  </SqlQuery>
</Command>
```

{% endcode %}

{% hint style="info" %}
Подробно почитать о функциях и операторах, поддерживаемых PostgreSQL для работы с JSON, можете по [ссылке](https://postgrespro.ru/docs/postgrespro/11/functions-json).
{% endhint %}

Функция сохранения заказа будет иметь вид:

```sql
CREATE OR REPLACE FUNCTION template.save_order(
    in_client_id bigint,
    in_order json)
  RETURNS TABLE(order_id bigint, order_number character varying) AS
$BODY$
DECLARE

  _order_id bigint = in_order ->> 'order_id';
  _order_date timestamp = convert_date_json((in_order ->> 'order_date')::timestamp);
  _description varchar = in_order->> 'description';

  _order_number varchar = lpad(floor(random() * 300)::varchar, 5, '0');

  _order_position record;

BEGIN

  IF (_order_id ISNULL) THEN
  
    INSERT INTO template.order (
      order_number,
      order_date,
      client_id,
      description,
      added
    )
    VALUES (
      _order_number,
      _order_date,
      in_client_id,
      _description,
      true
    )
    RETURNING "order".order_id INTO _order_id;
    
  ELSE

    UPDATE template.order
    SET
      order_number = _order_number,
      order_date = _order_date,
      client_id = in_client_id,
      description = _description
    WHERE
      order_id = _order_id;
      
  END IF;

  FOR _order_position IN (
    SELECT
       *
    FROM
      json_to_recordset(in_order -> 'order_position') AS T(
        order_position_id bigint,
        material_id bigint,
        quantity numeric,
        unit_price numeric
      )
  )
  LOOP

    IF (_order_position.order_position_id ISNULL) THEN
    
      INSERT INTO template.order_position (
        order_id,
        material_id,
        quantity,
        unit_price
      )
      VALUES (
        _order_id,
        _order_position.material_id,
        _order_position.quantity,
        _order_position.unit_price
      );
    
    ELSE

      UPDATE template.order_position
      SET
        material_id = _order_position.material_id,
        quantity = _order_position.quantity,
        unit_price = _order_position.unit_price
      WHERE
        order_position_id = _order_position.order_position_id;

    END IF;
  END LOOP;

  RETURN  QUERY SELECT _order_id, _order_number;
  
END;
$BODY$
  LANGUAGE plpgsql;
```

{% hint style="warning" %}
Обратите внимание: даты между клиентом и сервером передаются в UTC, и в JSON-объекте они не приводятся автоматически к часовому поясу сервера. Для корректного сохранения даты со временем используем функцию **public.convert\_date\_json()**.
{% endhint %}

Выполним тестовый запрос:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FIi0DWFtEXpPUPddVm5Fk%2Fimage.png?alt=media&#x26;token=2019520b-e7ec-4768-ac0c-5819297116b6" alt=""><figcaption></figcaption></figure>

### POST-запрос с файлами

Чтобы в кастомные API-запросы передавать файлы, HTTP-запрос должен использовать тип содержимого **multipart/form-data**.

Перейдем в Postman и создадим запрос *localhost:49707/data\_api/save\_client*:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FQPPIm3jjAZ7f02dBCQgb%2Fimage.png?alt=media&#x26;token=77b7b691-a5b6-4f97-bac1-6326028db62b" alt=""><figcaption></figcaption></figure>

Запрос содержит два объекта формы:

* client - json-объект с описанием данных клиента;
* file - передаваемый файл.

Объект client будет иметь вид:

```json
{
   "client_id":null,
   "city_id":3,
   "date_of_birth":"1994-07-22",
   "title":"Савушкин Степан Маркович",
   "email":"test123@mail.ru",
   "phone":"+79121234567"
}
```

Создадим ApiMethod для обработки такого запроса:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="SaveClientApiMethod" Route="save_client" Method="Put" VersionCode="1">
  <Parameters>
    <Parameter Name="client" Type="Json" Source="FormData"  Required="False" />
    <Parameter Name="file" Type="File" Source="FormData" />
  </Parameters>
  <Commands>
    <Command Name="SaveClientSqlQueryCommand" />
  </Commands>
  <Response>
    <Objects>
      <Object Name="client_id">
        <Command Name="SaveClientSqlQueryCommand"  Parameter="client_id"/>
      </Object>
    </Objects>
  </Response>
</ApiMethod>
```

{% endcode %}

В качестве источника данных укажем FormData.

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

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FGqwBNf3BzMQf0a4YrUWV%2Fimage.png?alt=media&#x26;token=6f723904-c3ca-4ed8-b2c8-30d1e98fca0e" alt=""><figcaption></figcaption></figure>

А в ApiMethod для параметра **file** добавить атрибут `Array` со значением True. В противном случае в запрос будет подставляться информация о последнем файле.

Команда SaveClientSqlQueryCommand будет иметь вид:

{% code title="Template.xml" %}

```xml
<Command Name="SaveClientSqlQueryCommand" Type="SqlQueryCommand">
  <SqlQuery>
    <Text>
      SELECT template.save_client({client}::json, {file}::json) AS client_id;
    </Text>
  </SqlQuery>
</Command>
```

{% endcode %}

Получив соответствующий запрос, сервер загрузит файл, сгенерирует guid и добавит информацию о нем в таблицу public.file. Для переменной **file** будет сгенерирована json-строка в виде массива объектов с двумя полями: **file\_name** (имя файла) и **guid** (сгенерированный идентификатор).&#x20;

Таким образом, текст запроса с замененными переменными будет иметь вид:

```sql
SELECT template.save_client('{
   "client_id":null,
   "city_id":3,
   "date_of_birth":"1994-07-22",
   "title":"Савушкин Степан Маркович",
   "email":"test123@mail.ru",
   "phone":"+79121234567"
}'::json, '[
  {
    "file_name": "ua1hNO_T_O8.jpg",
    "guid": "70b3464d-2731-4aef-9f52-89caa992109b"
  }
]'::json) AS client_id;
```

Создайте самостоятельно функцию **template.save\_client(json, json)**, которая будет создавать нового клиента или обновлять данные существующего. В качестве результата функция должна возвращать идентификатор клиента.

В ответ на запрос сервер вернет json-объект вида:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FnIl5eYVlmaO0D58RWZiy%2Fimage.png?alt=media&#x26;token=1dcc38cb-1c50-4da4-aeaa-f33eb83a5dca" alt=""><figcaption></figcaption></figure>

## Работа с файлами

### Загрузка файла

Для загрузки файла на сервер используется системный POST-запрос /files/upload, который в форме запроса должен иметь объект с именем file. После сохранения файла на сервере в ответе в поле FileGuid вернется сгенерированный guid.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FGnHOswBNPeQWrNL8dm2m%2Fimage.png?alt=media&#x26;token=0fcdb3af-97a8-4d0c-b599-3a9e16377d5d" alt=""><figcaption></figcaption></figure>

### Скачивание файла

Для скачивания файлов с сервера используется системный GET-запрос /files/download, ожидающий обязательный параметр fileGuid - guid файла, который генерируется автоматически при загрузке файла на сервер.

{% hint style="warning" %}
Запрос должен быть подписан токеном, полученным при авторизации пользователя в программе.
{% endhint %}

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FyUJy0GGwolziMv0n0GYI%2Fimage.png?alt=media&#x26;token=3089caca-55fe-47eb-ab35-b56b318eddbd" alt=""><figcaption></figcaption></figure>

## Блокировка API-запросов

Скорректируйте форму настроек, добавив объект CheckBox для включения/отключения модуля для работы с сайтом и кнопку для смены пароля у пользователя для сайта.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FiuudW3HLiFSmY4ciLNSL%2Fimage.png?alt=media&#x26;token=22d27f71-d030-43f9-a24b-f7b0d2d27b52" alt=""><figcaption></figcaption></figure>

По кнопке "Изменить пароль..." должна открываться форма смены пароля (TemplateUserPasswordEdit.xml), где для пользователя "Пользователь для сайта" (user\_name = 'USER\_FOR\_SITE') можно задать новый пароль. Измените самостоятельно логику смены пароля.

В таблице template.settings добавим колонку для включения/отключения модуля сайта:

```sql
ALTER TABLE template.settings
  ADD COLUMN site_module_is_on boolean NOT NULL DEFAULT false;
```

Теперь в файле Template.xml можем создать условие для проверки активности модуля сайта:

{% code title="Template.xml" %}

```xml
<Condition Name="SiteModuleIsOnSqlQueryCondition" Type="SqlQueryCondition">
  <Text>
    SELECT site_module_is_on FROM template.settings;
  </Text>
</Condition>
```

{% endcode %}

Добавим в GetUnitListApiMethod необязательный тэг `<Enabled>`, в котором укажем новое условие:

{% code title="Template.xml" %}

```xml
<ApiMethod Name="GetUnitListApiMethod" Route="unit_list" Method="Get" VersionCode="1">
  <Enabled>
    <Condition Name="SiteModuleIsOnSqlQueryCondition" />
  </Enabled>
  <Response>
    <Objects>
      <Object Name="units" Array="True">
        <Command Name="UnitListSelectSqlQueryCommand" />
      </Object>
    </Objects>
  </Response>
</ApiMethod>
```

{% endcode %}

Сделайте то же самое для остальных методов.

Если API-метод недоступен, то в ответ на запрос будет возвращаться код **404 Not Found**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FgmcZ2omVgDWn8izOACKv%2Fimage.png?alt=media&#x26;token=aca09971-42d2-49b3-b668-ac28c91b2717" alt=""><figcaption></figcaption></figure>

## Итоги <a href="#results" id="results"></a>

В этом уроке мы познакомились с возможностью создавать кастомные API-запросы для предоставления доступа сторонним сервисам, таким как сайты или мобильные приложения.

Помимо кастомных API-запросов рассмотрели штатные запросы для аутентификации пользователей и для работы с файлами.

Как маленький бонус: узнали о возможностях PostgreSQL по работе с JSON - в следующем уроке рассмотрим, как можно на форме создавать json-объекты для отправки их на сервер.

## Ответы <a href="#answer" id="answer"></a>

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

{% file src="<https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FPcI6WpVLTGrB5K3jrcRo%2Flesson25-answer.zip?alt=media&token=c3ba4cae-c0a8-4bdc-9e5c-caba27a6ae69>" %}

Ниже прикреплен экспортированный из Postman файл с коллекцией запросов:

{% file src="<https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FMYCbiluW9Z6RMcQv14eU%2FTemplate.postman_collection.json?alt=media&token=f964c424-0691-4a03-8388-ed808173d8a9>" %}

Чтобы импортировать коллекцию из файла, необходимо кликнуть по кнопке Import, и в отрывшемся окне выбрать файл коллекции.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2F2cUwfK20DL6kBv330QDk%2Fimage.png?alt=media&#x26;token=d8595fee-a7c4-4947-9692-8f4ef24ebe77" alt=""><figcaption></figcaption></figure>
