# Урок 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-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson25-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson25-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson25-answer.zip</a></td></tr></tbody></table>

Ниже прикреплен экспортированный из 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wfsys.gitbook.io/wt-practice/advanced/lesson_making_api_requests.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
