REST-эндпоинты
Каждая сущность в Apostol CRM предоставляет свой API через функцию-диспетчер в схеме rest. C++ сервер маршрутизирует входящие HTTP-запросы к этим PL/pgSQL процедурам, которые разбирают путь и тело запроса и делегируют обработку слою api.*.
Как работает маршрутизация
При получении POST /api/v1/sensor/list сервер:
- Убирает префикс
/api/v1/ - Извлекает имя сущности:
sensor - Находит зарегистрированный эндпоинт:
rest.sensor - Вызывает
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()-- путь равен NULLLoginFailed()-- отсутствует действительная сессияJsonIsEmpty()-- тело запроса равно NULL, когда оно обязательноObjectNotFound(entity, field, value)-- объект не существуетAccessDenied()-- недостаточно прав