REST-эндпоинты

Каждая сущность в Apostol CRM предоставляет свой API через функцию-диспетчер в схеме rest. C++ сервер маршрутизирует входящие HTTP-запросы к этим PL/pgSQL процедурам, которые разбирают путь и тело запроса и делегируют обработку слою api.*.

Как работает маршрутизация

При получении POST /api/v1/sensor/list сервер:

  1. Убирает префикс /api/v1/
  2. Извлекает имя сущности: sensor
  3. Находит зарегистрированный эндпоинт: rest.sensor
  4. Вызывает rest.sensor('/sensor/list', payload)

Процедура rest.sensor использует оператор CASE, чтобы направить путь к нужной API-функции.

Сигнатура функции

Каждый REST-диспетчер следует этой сигнатуре:

CREATE OR REPLACE FUNCTION rest.sensor (
  pPath       text,
  pPayload    jsonb default null
) RETURNS     SETOF json
AS $$
  • pPath -- путь URL (например, '/sensor/list')
  • pPayload -- тело JSON-запроса
  • Возвращает SETOF json -- каждая строка представляет один JSON-объект в массиве ответа

6 стандартных маршрутов

Каждая сущность реализует следующие маршруты:

/entity/type -- Доступные типы

Возвращает типы, определённые для данной сущности (например, default.sensor):

WHEN '/sensor/type' THEN
  FOR r IN SELECT * FROM jsonb_to_record(pPayload) AS x(fields jsonb)
  LOOP
    FOR e IN EXECUTE format('SELECT %s FROM api.type($1)',
      JsonbToFields(r.fields, GetColumns('type', 'api')))
      USING GetEntity('sensor')
    LOOP
      RETURN NEXT row_to_json(e);
    END LOOP;
  END LOOP;

/entity/method -- Доступные методы

Возвращает методы, доступные для конкретного объекта в зависимости от его текущего состояния:

WHEN '/sensor/method' THEN
  IF pPayload IS NULL THEN
    PERFORM JsonIsEmpty();
  END IF;

  arKeys := array_cat(arKeys, ARRAY['id']);
  PERFORM CheckJsonbKeys(pPath, arKeys, pPayload);

  FOR r IN SELECT * FROM jsonb_to_record(pPayload) AS x(id uuid)
  LOOP
    FOR e IN SELECT * FROM api.get_object_methods(r.id) ORDER BY sequence
    LOOP
      RETURN NEXT row_to_json(e);
    END LOOP;
  END LOOP;

/entity/count -- Количество объектов

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

WHEN '/sensor/count' THEN
  IF pPayload IS NOT NULL THEN
    arKeys := array_cat(arKeys, ARRAY['search', 'filter']);
    PERFORM CheckJsonbKeys(pPath, arKeys, pPayload);
  ELSE
    pPayload := '{}';
  END IF;

  FOR r IN SELECT * FROM jsonb_to_record(pPayload) AS x(search jsonb, filter jsonb)
  LOOP
    FOR e IN SELECT * FROM api.count_sensor(r.search, r.filter) AS count
    LOOP
      RETURN NEXT row_to_json(e);
    END LOOP;
  END LOOP;

/entity/set -- Создание или обновление (upsert)

Использует GetRoutines() для динамического определения имён параметров из сигнатуры функции:

WHEN '/sensor/set' THEN
  IF pPayload IS NULL THEN
    PERFORM JsonIsEmpty();
  END IF;

  arKeys := array_cat(arKeys, GetRoutines('set_sensor', 'api', false));
  PERFORM CheckJsonbKeys(pPath, arKeys, pPayload);

  FOR r IN EXECUTE format(
    'SELECT row_to_json(api.set_sensor(%s)) FROM jsonb_to_record($1) AS x(%s)',
    array_to_string(GetRoutines('set_sensor', 'api', false, 'x'), ', '),
    array_to_string(GetRoutines('set_sensor', 'api', true), ', ')
  ) USING pPayload
  LOOP
    RETURN NEXT r;
  END LOOP;

GetRoutines исследует сигнатуру функции во время выполнения:

  • GetRoutines('set_sensor', 'api', false) -- возвращает имена параметров как text[]
  • GetRoutines('set_sensor', 'api', true) -- возвращает пары name type для приведения к записи
  • GetRoutines('set_sensor', 'api', false, 'x') -- возвращает ссылки с префиксом x.name

/entity/get -- Получение одного объекта

Поддерживает проекцию полей через параметр fields:

WHEN '/sensor/get' THEN
  IF pPayload IS NULL THEN
    PERFORM JsonIsEmpty();
  END IF;

  arKeys := array_cat(arKeys, ARRAY['id', 'fields']);
  PERFORM CheckJsonbKeys(pPath, arKeys, pPayload);

  FOR r IN SELECT * FROM jsonb_to_record(pPayload) AS x(id uuid, fields jsonb)
  LOOP
    FOR e IN EXECUTE format('SELECT %s FROM api.get_sensor($1)',
      JsonbToFields(r.fields, GetColumns('sensor', 'api')))
      USING r.id
    LOOP
      RETURN NEXT row_to_json(e);
    END LOOP;
  END LOOP;

/entity/list -- Список с пагинацией

WHEN '/sensor/list' THEN
  IF pPayload IS NOT NULL THEN
    arKeys := array_cat(arKeys, ARRAY['fields', 'search', 'filter',
      'reclimit', 'recoffset', 'orderby']);
    PERFORM CheckJsonbKeys(pPath, arKeys, pPayload);
  ELSE
    pPayload := '{}';
  END IF;

  FOR r IN SELECT * FROM jsonb_to_record(pPayload) AS x(
    fields jsonb, search jsonb, filter jsonb,
    reclimit integer, recoffset integer, orderby jsonb)
  LOOP
    FOR e IN EXECUTE format('SELECT %s FROM api.list_sensor($1, $2, $3, $4, $5)',
      JsonbToFields(r.fields, GetColumns('sensor', 'api')))
      USING r.search, r.filter, r.reclimit, r.recoffset, r.orderby
    LOOP
      RETURN NEXT row_to_json(e);
    END LOOP;
  END LOOP;

Поддержка пакетных операций

Каждый маршрут поддерживает работу как с одним объектом, так и с пакетом объектов. Если тело запроса является JSON-массивом, используйте jsonb_to_recordset (множественное число) вместо jsonb_to_record:

IF jsonb_typeof(pPayload) = 'array' THEN
  FOR r IN SELECT * FROM jsonb_to_recordset(pPayload) AS x(id uuid)
  LOOP
    -- обработка каждого элемента
  END LOOP;
ELSE
  FOR r IN SELECT * FROM jsonb_to_record(pPayload) AS x(id uuid)
  LOOP
    -- обработка одного элемента
  END LOOP;
END IF;

Проекция полей

Клиенты могут запрашивать только определённые столбцы:

{
  "id": "...",
  "fields": ["id", "code", "label"]
}

Функция JsonbToFields(r.fields, GetColumns('sensor', 'api')) проверяет запрошенные поля по фактическим столбцам представления и возвращает список столбцов SQL. Если fields равно NULL, она возвращает *.

Динамическая делегация методов

Заключительная ветка ELSE автоматически обрабатывает действия рабочего процесса:

ELSE
  RETURN NEXT ExecuteDynamicMethod(pPath, pPayload);

Это обрабатывает пути вида /sensor/enable, /sensor/disable, /sensor/delete, /sensor/restore, а также любые пользовательские действия, зарегистрированные в рабочем процессе. ExecuteDynamicMethod извлекает действие из пути, находит метод для класса объекта и его текущего состояния и выполняет его.

Проверка ключей

Перед обработкой следует проверить ключи в теле запроса, чтобы выявить опечатки:

arKeys := array_cat(arKeys, ARRAY['id', 'fields']);
PERFORM CheckJsonbKeys(pPath, arKeys, pPayload);

CheckJsonbKeys вызывает ошибку, если тело запроса содержит неизвестные ключи.

Регистрация маршрута

В файле init.sql сущности зарегистрируйте маршрут:

PERFORM RegisterRoute('sensor', AddEndpoint('SELECT * FROM rest.sensor($1, $2);'));

Это сопоставляет префикс URL /sensor/* с процедурой rest.sensor.

Добавление пользовательских маршрутов

Помимо 6 стандартных маршрутов вы можете добавлять маршруты, специфичные для конкретной сущности:

WHEN '/client/balance' THEN
  IF pPayload IS NULL THEN
    PERFORM JsonIsEmpty();
  END IF;

  arKeys := array_cat(arKeys, ARRAY['id']);
  PERFORM CheckJsonbKeys(pPath, arKeys, pPayload);

  FOR r IN SELECT * FROM jsonb_to_record(pPayload) AS x(id uuid)
  LOOP
    RETURN NEXT api.get_client_balance(r.id);
  END LOOP;

Обработка ошибок

Ошибки обрабатываются системой исключений Платформы. Функции возбуждают исключения (через PERFORM SomeError() или RAISE EXCEPTION), а слой REST возвращает стандартный ответ с ошибкой. Распространённые функции ошибок:

  • RouteIsEmpty() -- путь равен NULL
  • LoginFailed() -- отсутствует действительная сессия
  • JsonIsEmpty() -- тело запроса равно NULL, когда оно обязательно
  • ObjectNotFound(entity, field, value) -- объект не существует
  • AccessDenied() -- недостаточно прав