Отчёты (API)
REST API конструктора отчётов: управление определениями, выполнение, экспорт и расписания. Все маршруты требуют аутентификации (Bearer-токен, см. Аутентификация).
Эндпоинты
| Метод | URL | Назначение |
|---|---|---|
GET | /api/sys/reports/entities | Сущности текущего тенанта, доступные для отчётов |
GET | /api/sys/reports/meta/{sysTableId} | Дерево полей сущности (включая поля связанных) и источники агрегатов |
GET | /api/sys/reports/sql/schema | Таблицы и колонки, доступные SQL-отчётам тенанта |
POST | /api/sys/reports/sql/preview | Выполнить несохранённый SQL (первые строки) |
GET | /api/sys/reports | Список доступных отчётов |
POST | /api/sys/reports | Создать отчёт |
GET | /api/sys/reports/{id} | Получить отчёт |
PUT | /api/sys/reports/{id} | Обновить отчёт |
DELETE | /api/sys/reports/{id} | Удалить отчёт (вместе с расписаниями и файлами) |
GET/PUT | /api/sys/reports/{id}/assignments | Доступ: назначения на пользователей/роли/группы |
POST | /api/sys/reports/{id}/data | Выполнить синхронно (HTML-просмотр, до 10 000 строк) |
POST | /api/sys/reports/{id}/export | Запустить фоновый экспорт (csv / xlsx / pdf) |
GET | /api/sys/reports/{id}/runs | История выгрузок |
GET | /api/sys/report-runs/{runId} | Статус выгрузки (queued / running / done / failed) |
GET | /api/sys/report-runs/{runId}/download | Скачать готовый файл |
GET/POST | /api/sys/reports/{id}/schedules | Расписания отчёта |
PUT/DELETE | /api/sys/report-schedules/{scheduleId} | Изменить/удалить расписание |
POST | /api/sys/report-schedules/{scheduleId}/run-now | Выполнить расписание немедленно |
Права: reports.build — создание/редактирование своих отчётов конструктора, reports.sql — своих SQL-отчётов, reports.view — выполнение назначенных, admin.reports (роль report_administrator) — все отчёты тенанта. Данные отчёта конструктора фильтруются правилами доступа к записям выполняющего пользователя; расписания выполняются от имени владельца отчёта. SQL-отчёт правилами доступа к записям не фильтруется — см. SQL-отчёты.
Структура определения (definition)
{
"columns": [
{ "field": "number" },
{ "field": "title", "label": { "ru": "Тема", "en": "Subject" } },
{ "path": ["client_id"], "field": "name" },
{
"type": "related_aggregate",
"fn": "count",
"relation": { "kind": "reverse_field", "entity": "deals", "field": "client_id" },
"conditions": [
{ "field": "status", "operator": "=", "value": "open" }
],
"label": { "ru": "Открытых сделок" }
}
],
"filters": [
{
"id": "created_period",
"field": "created_at",
"operator": "relative_period",
"value": { "period": "last_week" },
"is_parameter": true,
"required": true
}
],
"group_by": [
{ "field": "assignee_id", "order": "asc" }
],
"aggregations": [
{ "fn": "count", "field": "*" },
{ "fn": "p90", "field": "resolution_hours" }
],
"sort": [
{ "field": "created_at", "dir": "desc" }
]
}Ключевые правила (проверяются валидатором при сохранении):
columns— обязательна хотя бы одна колонка. Поле связанной сущности задаётся черезpath— список ссылочных полей-переходов от основной сущности (не более 2 переходов). Колонок типаrelated_aggregate— не более 10.filters.operator— один из:=,!=,like,>,<,>=,<=,in,date_range,relative_period,between,is_null,not_null,category,exists_related. Фильтр сis_parameter: trueможет быть переопределён при выполнении.group_by— не более 3 уровней,order—asc|desc.aggregations.fn—count,sum,avg,min,maxили перцентильpNN(p50,p90,p95,p99, …). Все, кромеcount, требуют числовое поле; агрегируемое поле должно присутствовать вcolumns— итог выводится в этой колонке."field": "*"допустим только дляcount.related_aggregate—fnте же; для не-countобязателен числовойtarget_fieldдочерней сущности;conditionsподдерживают операторы=,!=,like,>,<,>=,<=,in,date_range,is_null,not_null.
Примеры
Создать отчёт
curl -X POST "$ORBITA_URL/api/sys/reports" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Заявки за неделю по исполнителям",
"sys_table_id": 12,
"definition": {
"columns": [
{ "field": "number" },
{ "field": "title" },
{ "field": "assignee_id" }
],
"filters": [
{ "id": "period", "field": "created_at", "operator": "relative_period",
"value": { "period": "last_week" }, "is_parameter": true }
],
"group_by": [{ "field": "assignee_id", "order": "asc" }],
"aggregations": [{ "fn": "count", "field": "*" }]
}
}'Выполнить с параметрами
Значения параметров передаются по id фильтра:
curl -X POST "$ORBITA_URL/api/sys/reports/5/data" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "parameters": { "period": { "period": "last_n_days", "n": 30 } } }'Экспорт и скачивание
Экспорт асинхронный: запрос ставит задачу в отдельную очередь reports, ответ содержит run. Дальше — опрос статуса и скачивание:
RUN_ID=$(curl -s -X POST "$ORBITA_URL/api/sys/reports/5/export" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "format": "xlsx" }' | jq -r '.data.id')
# статус: queued → running → done | failed
curl -s "$ORBITA_URL/api/sys/report-runs/$RUN_ID" -H "Authorization: Bearer $TOKEN"
curl -L -o report.xlsx "$ORBITA_URL/api/sys/report-runs/$RUN_ID/download" \
-H "Authorization: Bearer $TOKEN"Лимиты форматов: XLSX — 50 000 строк, PDF — 1 000 строк (настраиваются переменными REPORTS_XLSX_MAX_ROWS / REPORTS_PDF_MAX_ROWS, см. Переменные окружения). Файлы хранятся 30 дней (REPORTS_RUN_TTL_DAYS).
SQL-отчёты
Отчёт с "type": "sql" хранит запрос в definition.sql; sys_table_id у него нет. Тип задаётся при создании и в PUT не меняется.
curl -X POST "$ORBITA_URL/api/sys/reports" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "sql",
"title": { "ru": "Заявки за период" },
"definition": {
"sql": "SELECT status, count(*) AS n FROM incidents WHERE created_at >= :p_from AND created_at < :p_to AND (:team IS NULL OR team = :team) GROUP BY 1",
"parameters": [
{ "name": "p", "type": "period", "label": { "ru": "Период" }, "default": { "preset": "last_week" } },
{ "name": "team", "type": "text", "label": { "ru": "Команда" }, "required": false }
]
}
}'Параметр: name (латиница, цифры, _), type — text | number | date | period, label, required, default. Параметр типа period подставляется как :<name>_from / :<name>_to; значение — { "preset": "last_week" } или { "preset": "last_n_days", "amount": 30 }. В /data, /export и /sql/preview значения передаются по имени: { "parameters": { "team": "L2", "p": { "preset": "this_month" } } }.
Предпросмотр несохранённого запроса (нужно право reports.sql):
curl -X POST "$ORBITA_URL/api/sys/reports/sql/preview" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "sql": "SELECT count(*) FROM incidents", "parameters": [], "values": {} }'Ответ — columns, rows (как у /data) и meta.elapsed_ms; строк не больше REPORTS_SQL_PREVIEW_ROWS.
Ошибки SQL-отчёта приходят с кодом 422 и полями error_key (например multiple_statements, unknown_parameter, parameter_required, timeout, database), error_params и, для ошибок Postgres, sql_state. Текст ошибки Postgres — в error_params.detail, номера строк в нём относятся к тексту запроса.
Как выполняется запрос
- Для каждого тенанта в базе создаются роль
orbita_rpt_<хеш базы>_t<id тенанта>(LOGIN, без членства в других ролях) и схемаrpt_t<id тенанта>с представлениями — по одному на сущность, связь многие-ко-многим и справочник (sys_users,sys_companies,sys_org_units,sys_field_options). Запрос выполняется на отдельном подключении под этой ролью, в транзакции только для чтения, сstatement_timeout. - Роль получает
SELECTтолько на таблицы сущностей и связей своего тенанта. Общие таблицы (users,sys_companies,sys_org_units) она читает через функцииSECURITY DEFINERс фильтром по тенанту. - Представления пересоздаются, когда меняется состав сущностей, их поля или физические таблицы; отпечаток текущего состава хранится в комментарии схемы. Удалению таблицы сущности схема отчётов не мешает.
- Пароль роли вычисляется из
APP_KEY, в базе хранится только SCRAM-хеш. При удалении данных тенанта роль и схема удаляются.
Требования к базе: пользователю приложения нужно право CREATEROLE, а pg_hba.conf должен пускать новые роли по паролю с хоста приложения. В compose-файлах поставки приложение подключается под суперпользователем контейнера postgres:16-alpine, у которого есть и то, и другое. На PostgreSQL ниже 16 проверка значений при приведении типов колонок идёт по регулярным выражениям вместо pg_input_is_valid().
Расписание рассылки
curl -X POST "$ORBITA_URL/api/sys/reports/5/schedules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cron_expression": "0 8 * * 1",
"timezone": "Europe/Moscow",
"format": "xlsx",
"recipients": [
{ "type": "user", "id": 7 },
{ "type": "email", "value": "cfo@example.com" }
],
"parameters": { "period": { "period": "last_week" } },
"is_active": true
}'Файл до 10 МБ уходит вложением, крупнее — подписанной ссылкой (срок жизни 7 дней). Письма отправляются от имени владельца отчёта и с его правами на данные.
Очередь reports
Экспорт и расписания обрабатывает выделенная очередь reports (соединение database_reports или redis_reports — по драйверу очередей инсталляции). Убедитесь, что воркер этой очереди запущен: php artisan queue:work database_reports --queue=reports.