Child pages
  • Туториал по написанию API для модуля

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Данная страница документация не завершена.

Актуально для версии 1.22.00.

...

Поскольку практически каждая функция администратора должна иметь CRUD-составляющую, то вот как это выглядит для /portal/articles

Code Block
languageperl
titleCRUD роуты
collapsetrue
  return [
    # Создать статью
    {
      method      => 'POST',
      path        => '/portal/articles/',
      # Параметры до валидатора
      params      => POST_PORTAL_ARTICLES,
      controller  => 'Portal::Api::admin::Articles',
      endpoint    => \&Portal::Api::admin::Articles::post_portal_articles,
      credentials => [
        'ADMIN', 'ADMINSID'
      ]
    }, 
    # Получить статьи
    {
      method      => 'GET',
      path        => '/portal/articles/',
      controller  => 'Portal::Api::admin::Articles',
      endpoint    => \&Portal::Api::admin::Articles::get_portal_articles,
      credentials => [
        'ADMIN', 'ADMINSID'
      ]
    },
    # Получить конкретную статью
    {
      method      => 'GET',
      path        => '/portal/articles/:id/',
      controller  => 'Portal::Api::admin::Articles',
      endpoint    => \&Portal::Api::admin::Articles::get_portal_articles_id,
      credentials => [
        'ADMIN', 'ADMINSID'
      ]
    },
    # Изменить конкретную статью
    {
      method      => 'PUT',
      path        => '/portal/articles/:id/',
      controller  => 'Portal::Api::admin::Articles',
      endpoint    => \&Portal::Api::admin::Articles::put_portal_articles_id,
      credentials => [
        'ADMIN', 'ADMINSID'
      ]
    },
    # Удалить конкретную статью
    {
      method      => 'DELETE',
      path        => '/portal/articles/:id/',
      controller  => 'Portal::Api::admin::Articles',
      endpoint    => \&Portal::Api::admin::Articles::delete_portal_articles_id,
      credentials => [
        'ADMIN', 'ADMINSID'
      ]
    },
  ]

...

Code Block
languageperl
titleUSER API роуты
collapsetrue
#**********************************************************
=head2 user_routes() - Returns available USER API paths

=cut
#**********************************************************
sub user_routes {
  my $self = shift;

  return [
    {
      method      => 'GET',
      # Для USER API ОБЯЗАТЕЛЬНО начинаем абсолютный путь с /user/*.
      path        => '/user/portal/menu/',
      # Подключаем "контроллер" для API /user/portal/*
      controller  => 'Portal::Api::user::News',
      # Даём ссылку на функцию-эндпойнт контроллера
      endpoint    => \&Portal::Api::user::News::get_user_portal_news,
      credentials => [
	    # Определяем нужные параметры для авторизации.
        # USER    - авторизация по header, полученном с /user/login
        # USERSID - авторизация по cookie (в том числе для api_call)
        # PUBLIC  - без авторизации
        # Тоесть, в данном случае путь может работать как и с авторизованными пользователями, так и нет.
        # Внутри хэндлера можно определять какой пользователь, об этом ниже.
        'USER', 'USERSID', 'PUBLIC'
      ]
    },
  ]
}

...

Code Block
languageperl
titleконтроллер Portal::Api::user::News
collapsetrue
package Portal::Api::user::News;

=head1 NAME 

  User Portal

  # Рекомендуем в подах записывать к каким
  # группам эндпойнтов относится данный контроллер
  
  Endpoints:
    /user/portal/news*
    /user/portal/menu

=cut

use strict;
use warnings FATAL => 'all';

use Control::Errors;
# Импортируем объект Portal для работы с базой
# он должен находиться в /usr/abills/Abills/mysql/Portal.pm
use Portal;

my Portal $Portal;
my Control::Errors $Errors;

#**********************************************************
=head2 new($db, $admin, $conf)

=cut
#**********************************************************
sub new {
  my ($class, $db, $admin, $conf, $attr) = @_;

  my $self = {
    db    => $db,
    admin => $admin,
    conf  => $conf,
    attr  => $attr
  };

  bless($self, $class);

  $Portal = Portal->new($db, $admin, $conf);

  # Определяем словарь ошибок, который нам пришёл выше
  $Errors = $self->{attr}->{Errors};

  return $self;
}

# здесь определять пути

1;

...

В целом, определение роутов для USER API ничем не отличается от ADMIN API, кроме одной важной детали - в $path_params при авторизации будет приходить uid.
Очень важная составляющая.

Code Block
languageperl
collapsetrue
 
#**********************************************************
=head2 get_user_portal_news($path_params, $query_params)

  Endpoint GET /user/portal/news

=cut
#**********************************************************
sub get_user_portal_news {
  my $self = shift;
  my ($path_params, $query_params) = @_;

  # Не обязательно писать всю логику прямо внутри эндпоинта, как в примере с ADMIN API
  # Вы можете делить логику в бизнес-функции, для сокращения использования.
  # Но для простоты понимания, с самого начала лучше писать всё в эндпоинтах
  
  return $self->_portal_menu({
    # Если пользователь авторизован - в $path_params->{uid} будет UID пользователя.
    # Если нет - поле будет пустое.
    UID       => $path_params->{uid} || '',
    DOMAIN_ID => $query_params->{DOMAIN_ID},
    PORTAL_MENU_ID => $query_params->{PORTAL_MENU_ID},
    MAIN_PAGE => $query_params->{MAIN_PAGE},
    LIST      => 1
  });
} 

Практики

Поиск, сортировка, пагинация

Для правильной работы сортировки мы рекомендуем делать это не вручну, а с помощью search_former которые находятся внутри любого современного модуля.
Он выглядит приблизительно так, с точки зрения пути GET /portal/menus

...

Написание OpenAPI

Каждое API нужно описывать.

Поэтому, по нашему микрофреймворку над OpenAPI

  • Файл Api/swagger/(admin|user)/paths.yaml - основа
  • Папка Api/swagger/(admin|user)/paths - определения путей
  • Папка Api/swagger/(admin|user)/schemas - определения схем

Файл user/paths.yaml:

Code Block
languageyml
titleuser/paths.yaml
/user/portal/news:
  $ref: "./paths/news.yaml"

И в ./paths/news.yaml:

Code Block
languageyml
titlepaths/news.yaml
collapsetrue
get:
  tags:
    - portal
  summary: Список новостей
  responses:
    200:
      description: Успешное выполнение
      content:
        application/json:
          schema:
            $ref: "../schemas/news.yaml"
  security:
    - USERSID: [ ]

schemas/news.yaml:

Code Block
languageyml
titleschemas/news.yaml
collapsetrue
type: object
properties:
  news:
    type: array
    items:
      type: object
      properties:
        content:
          type: string
          example: При замовленні пакету «ABillS+» платіть X грн/міс замість X грн/міс. Підключайте ТВ-пакет «База+» і отримаєте в подарунок півроку користування послугою! Кожен місяць сплачуйте лише 50% вартості пакету. Абонплата складе всього X грн.
        date:
          type: string
          format: date
          example: 2023-01-23
        id:
          type: number
          example: 4
        importance:
          type: number
          example: 1
        onMainPage:
          type: number
          example: 1
        permalink:
            type: string
            example: special-promotion-50-2023
        picture:
          type: string
          example: "http://192.168.99.2:9443/images/attach/portal/9700077.png"
        shortDescription:
          type: string
          example: Тарифний план для всіх у сімї
        title:
          type: string
          example: Акціний тариф візьми поки можеш
        topicId:
          type: number
          example: 1
  topics:
    type: array
    items:
      type: object
      properties:
        id:
          type: number
          example: 1
        name:
          type: string
          example: Акції від нас
        url:
          type: string
          example: "https://demo.abills.net.ua:9443/"

И в конце запускаем misc/api/generate_docs.pl, и проверяем bundle_user.yaml.

И поздравляем, вы полностью разработали и описали USER API для модуля.

Практики

Поиск, сортировка, пагинация

Для правильной работы сортировки мы рекомендуем делать это не вручну, а с помощью search_former которые находятся внутри любого современного модуля.
Он выглядит приблизительно так, с точки зрения пути GET /portal/menus

Code Block
languageperl
collapsetrue
#**********************************************************
=head2 get_portal_menus($path_params, $query_params)

  Endpoint GET /portal/menus

=cut
#**********************************************************
sub get_portal_menus {
  my $self = shift;
  my ($path_params, $query_params) = @_;

  # Проверяем, определяем стандартные параметры для вызова
  my %PARAMS = (
    COLS_NAME => 1,
    PAGE_ROWS => $query_params->{PAGE_ROWS} ? $query_params->{PAGE_ROWS} : 25,
    SORT      => $query_params->{SORT} ? $query_params->{SORT} : 1,
    PG        => $query_params->{PG} ? $query_params->{PG} : 0,
    DESC      => $query_params->{DESC},
  );

  foreach my $param (keys %{$query_params}) {
    $query_params->{$param} = ($query_params->{$param} || "$query_params->{$param}" eq '0')
      ? $query_params->{$param}
      : '_SHOW';
  }

  my $list = $Portal->portal_menu_list({
    ID => '_SHOW',
    NAME => '_SHOW',
    URL => '_SHOW',
    DATE => '_SHOW',
    STATUS => '_SHOW',
    # С помощью внутренней деструктуризации присваиваем
    %$query_params,
    # Оверрайдим параметры
    %PARAMS,
  });

  return {
    list  => $list,
    total => $Portal->{TOTAL}
  };
}

И смотрим что происходит под капотом у portal_menu_list

Code Block
languageperl
collapsetrue
#**********************************************************
=head2 function portal_menu_list() - get menu section list

  Arguments:
    $attr

  Returns:
    \@list -
  Examples:
    my $list = $Portal->portal_menu_list({COLS_NAME=>1});

=cut
#**********************************************************
sub portal_menu_list {
  my $self = shift;
  my ($attr) = @_;

  # Преопределяем параметры, если их нет
  my $SORT = ($attr->{SORT}) ? $attr->{SORT} : 1;
  my $DESC = ($attr->{DESC}) ? $attr->{DESC} : '';
  my $PG = $attr->{PG} ? $attr->{PG} : 0;
  my $PAGE_ROWS = $attr->{PAGE_ROWS} ? $attr->{PAGE_ROWS} : 25;

  # Эта функция на основе пришедших параметров и паттерном формирует $WHERE clause.
  my $WHERE = $self->search_former($attr, [
    [ 'ID',       'INT',   'pm.id',                   1 ],
    [ 'NAME',     'STR',   'pm.name',                 1 ],
    [ 'URL',      'STR',   'pm.url',                  1 ],
    [ 'DATE',     'STR',   'DATE(pm.date) as date',   1 ],
    [ 'STATUS',   'INT',   'pm.status',               1 ],
  ], { WHERE => 1 });

  $self->query(
    "SELECT $self->{SEARCH_FIELDS} pm.id
      FROM portal_menu pm
      $WHERE
      ORDER BY $SORT $DESC LIMIT $PG, $PAGE_ROWS;;",
    undef, $attr
  );

  my $list = $self->{list} || [];

  # Берём общий count
  $self->query("SELECT COUNT(*) AS total FROM portal_menu pm
    $WHERE;",
    undef,
    { INFO => 1 }
  );

  return $list || [];
}

...