Серверная логика

Запрос как хранимая процедура

Запрос в Интеграме не только читает данные, но и меняет их: колонка с выражением «Присвоить» записывает значение в каждую отобранную строку, создаёт или удаляет записи. Логика хранится в самой базе, а роли получают право вызвать запрос, а не право писать в таблицы. Это аналог хранимой процедуры: изменения идут только так, как их описал автор запроса, и только в тех строках, которые он разрешил.

Здесь — как собрать такой запрос, вызвать его, ограничить круг вызывающих и строки, которые им доступны, и сделать вызов атомарным. Примеры даны для https://ideav.ru и базы mydb; $TOKEN и $XSRF — токены из справочника API.

Как это устроено

Зачем процедура вместо прав на таблицу

Право WRITE на таблицу разрешает менять любое поле любой доступной записи. Часто нужно другое: менеджер видит статус заказа, но ставит «Согласован» только кнопкой, только в своих заказах, и вместе со статусом обязательно пишется дата согласования. Такое правило не выразить уровнем доступа — его выражают запросом.

  • поле в роли — только чтение, менять его может лишь запрос;
  • роль получает доступ READ к самому запросу — этого достаточно, чтобы его вызвать;
  • запрос сам решает, какие строки трогать и что в них писать: фильтры внутри запроса вызывающий не переопределит;
  • каждое изменение попадает в журнал, а с галкой InTransaction вызов выполняется целиком или не выполняется вовсе.

Что проверяется при вызове

ПроверкаГде задаётся
Может ли пользователь вызвать запросДоступ роли к запросу (таблица 116): без него — ответ 403
Какие строки будут измененыФильтры колонок запроса (t102/t103), в том числе [USER_ID], [ROLE_ID]
Что будет записаноВыражения «Присвоить» (t132) — их задаёт только автор запроса
Что может подставить вызывающийТолько фильтры колонок с «Именем в отчёте» (t100) и условие WHERE, которое лишь сужает выборку
Что изменение намеренноеPOST с confirmed=1 и _xsrf; без них — только превью

Права роли на таблицы при выполнении запроса не проверяются: запрос пишет туда, куда его направил автор. Поэтому вся защита — в определении запроса, и править его могут только администраторы базы.

Собрать запрос-процедуру

Колонка «Присвоить»

У колонки запроса есть поле «Присвоить» (t132) — выражение, которое вычисляется для каждой отобранной строки и записывается в эту колонку. Запрос, у которого есть хотя бы одно такое выражение, — изменяющий.

Выражение в t132Что запишется
'Согласован'Строка — в SQL-кавычках
[TODAY], [NOW], [USER_ID], [USER]Дата, время, id или логин вызывающего
[THIS] + 1, substring([THIS],1,10)Выражение от текущего значения колонки
CONCAT(Имя, ' ', Фамилия)SQL-выражение над псевдонимами других колонок (t101)
12345 в колонке-справочникеСсылка на запись справочника по id
[Тонер,картридж] в мультиссылкеНабор ссылок: каждое значение ищется в справочнике по тексту
пустое значениеУдаление значения, а у колонки главной таблицы — удаление записи

Текст в колонке-справочнике ищется по названию, и отсутствующее значение создаётся. Опечатка в выражении заведёт в справочнике новую запись — надёжнее писать id.

Пример: согласование заказа

Запрос approve_order: ставит заказу статус «Согласован» и дату согласования. Менеджер может согласовать только свой заказ и только один за вызов. Коды для примера: таблица Заказ — 900, её колонки Статус — 901, Дата согласования — 902, Менеджер (ссылка на пользователя) — 903, роль «Менеджер» — 164.

# сам запрос
curl -s "https://ideav.ru/mydb/_m_new/22?JSON=1" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" -F "up=1" -F "t22=approve_order"
# → {"id": 950}

# 1. какой заказ: параметр order_id, без него не отбирается ни одна строка
curl -s "https://ideav.ru/mydb/_m_new/28?JSON=1&up=950" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=950" -F "t28=900" -F "t100=order_id" -F "t104=abn_ID" -F "t102=0"

# 2–3. что записать
curl -s "https://ideav.ru/mydb/_m_new/28?JSON=1&up=950" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=950" -F "t28=901" -F "t100=Статус" -F "t132='Согласован'"
curl -s "https://ideav.ru/mydb/_m_new/28?JSON=1&up=950" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=950" -F "t28=902" -F "t100=Дата согласования" -F "t132=[TODAY]"

# 4. ограничитель: только заказы вызывающего. Без t100 — снаружи не переопределить
curl -s "https://ideav.ru/mydb/_m_new/28?JSON=1&up=950" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=950" -F "t28=903" -F "t104=abn_ID" -F "t102=[USER_ID]" -F "t107=1"

# 5. роли «Менеджер» — доступ READ к запросу
curl -s "https://ideav.ru/mydb/_m_new/116?JSON=1" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=164" -F "t116=950" -F "t136=53"

Поля Статус и Дата согласования роли выдают на чтение (t136=53): руками их не поменять, только этим запросом. Так закрепляется бизнес-правило — «согласовать» значит пройти через кнопку со всеми её условиями.

Ограничители, которые не подменить

Снаружи фильтруются только колонки с заполненным «Именем в отчёте» (t100): параметры FR_{имя} и TO_{имя} перекрывают их t102/t103. Фильтр колонки без имени параметрами адреса не меняется — это и есть защита от подмены фильтра вызывающим.

  • ограничитель — колонка без t100, обычно скрытая (t107=1): [USER_ID] на колонке-ссылке на пользователя, статус, дата;
  • параметр — колонка с t100: то, что вызывающий вправе выбирать, например id заказа;
  • значение параметра понимает синтаксис фильтров: FR_order_id=% отберёт все заказы, >0 — тоже. Поэтому охват всегда держит ограничитель без имени, а не параметр;
  • условие WHERE из адреса вызова встаёт в выборку в скобках как AND (…) и может только сузить её. Строка с лишней закрывающей скобкой, незакрытыми кавычками, ; или комментарием отклоняется.

Не давайте колонке-ограничителю «Имя в отчёте». С именем её фильтр становится параметром: вызывающий подставит чужой id вместо [USER_ID] и изменит чужие строки.

Повторный вызов без вреда

Охват изменения — строки под фильтрами. Добавьте фильтр «ещё не обработано», и повторный прогон не тронет уже изменённые строки: например, скрытая колонка результата с t102=!% (пусто). Так безопасно перезапускать массовые обработки — нормализацию названий, разметку, пересчёт.

# колонка «Токены» (731): заполнять только пустые; Название — псевдоним (t101) другой колонки
curl -s "https://ideav.ru/mydb/_m_new/28?JSON=1&up=960" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=960" -F "t28=731" -F "t102=!%" \
  -F "t132=LOWER(Название)"

Вызов

Превью: что изменится

Вызов изменяющего запроса без подтверждения ничего не пишет. В интерфейсе отчёта он показывает строки под фильтром и ссылку «Выполнить»; через API — те же строки с текущими значениями.

curl -s -H "X-Authorization: $TOKEN" "https://ideav.ru/mydb/report/approve_order?JSON_KV&FR_order_id=1234"

Применение: confirmed=1

Изменения применяются POST-запросом с полем confirmed=1 ко всем строкам под фильтром. Подтверждённый вызов проверяет _xsrf — и при авторизации токеном тоже: без него сервер отвечает 403 «Неверный или устаревший токен CSRF». Ответ — строки уже с новыми значениями.

curl -s "https://ideav.ru/mydb/report/approve_order?JSON_KV" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "confirmed=1" -F "FR_order_id=1234"

Кнопка в рабочем месте делает то же самое: POST на report/<запрос> с телом confirmed=1&FR_order_id=…&_xsrf=….

EXECUTE: без подтверждения

Булев реквизит запроса EXECUTE снимает оба шага: запрос применяет изменения при любом вызове, GET или POST, без confirmed и без проверки XSRF. Встроенный в рабочее место EXECUTE-запрос срабатывает при каждом открытии страницы — это автоматизация без кнопки.

Реквизит распознаётся по имени. Если у таблицы «Запрос» его нет, его добавляют как обычную булеву колонку EXECUTE.

EXECUTE пишет данные при каждом вызове, в том числе по ссылке. Для изменений, которые нельзя выполнить случайно, оставляйте двухшаговый вызов с confirmed=1.

Кто и как вызывает

  • кнопка или действие рабочего места — POST с confirmed=1;
  • открытие страницы с встроенным EXECUTE-запросом;
  • внешняя система по расписанию — своей учётной записью с доступом к одному запросу, а не к таблицам;
  • ИИ-агент и интеграции — через тот же report/ своей учётной записью и с правами её роли;
  • триггер кнопки при создании, изменении или удалении записи — см. «Триггеры кнопок».

Права вызывающего

Кто может вызвать

Вызов разрешён роли с доступом к запросу — записью таблицы 116: t116={id запроса}, t136=53 (READ). Доступ ко всей таблице «Запрос» (22) открывает все запросы сразу. Без доступа сервер отвечает 403; пользователь admin проходит всегда.

curl -s "https://ideav.ru/mydb/_m_new/116?JSON=1" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=164" -F "t116=950" -F "t136=53"

Условия по пользователю и роли

Встроенные переменные подставляются значениями вызывающего в любой фильтр и выражение запроса — так проверка прав живёт внутри процедуры:

ПеременнаяЗначение
[USER_ID], [USER]id и логин вызывающего
[ROLE_ID], [ROLE]id и название его роли; [ROLE] в формуле берут в кавычки: '[ROLE]'
[TODAY], [NOW], [MONTH_AGO]дата и время вызова
[Имя запроса]значение другого запроса — например, список разрешённых id

«Только свои записи» — ограничитель t102=[USER_ID] на колонке-ссылке на пользователя (см. пример выше). «Только для ролей» — скрытая вычисляемая колонка-условие без имени: для чужой роли запрос не отберёт ни одной строки и ничего не изменит.

# менять могут только роли 164 и 170
curl -s "https://ideav.ru/mydb/_m_new/28?JSON=1&up=950" \
  -H "X-Authorization: $TOKEN" -F "_xsrf=$XSRF" \
  -F "up=950" -F "t28=0" -F "t101=[ROLE_ID] IN (164,170)" -F "t102=1" -F "t107=1"

Поле только для чтения, меняет кнопка

Чтобы поле менялось только процедурой, роли выдают на него READ (t136=53), а запрос с «Присвоить» на это поле — READ на сам запрос. Так закрепляются статусы, даты согласования, суммы к оплате: правило проверки — в запросе, обойти его правкой карточки нельзя.

Кто правит сами процедуры

Определение запроса — записи таблиц «Запрос» (22) и «Колонки запроса» (28). Кто может их менять, тот может написать любое «Присвоить» в любое поле. Ролям пользователей WRITE на таблицы 22 и 28 не выдают: процедуры правят только администраторы базы.

Целостность

InTransaction: всё или ничего

Булев реквизит запроса InTransaction выполняет вызов одной транзакцией: выборка и все её изменения — присвоения, создание и удаление записей — фиксируются вместе, а ошибка посреди вызова откатывает уже записанное.

  • действует только на применение изменений: у запроса есть «Присвоить», и вызов идёт с confirmed=1 или с EXECUTE; превью и чистая выборка идут без транзакции;
  • вложенный запрос, вызванный внутри открытой транзакции, свою не открывает;
  • реквизит распознаётся по имени; если у таблицы «Запрос» его нет, добавьте булеву колонку InTransaction, как и EXECUTE.

Без галки каждая запись фиксируется сразу, и упавший вызов оставляет сделанное до ошибки. Тогда выручают фильтр «ещё не обработано» и журнал.

Журнал изменений

Каждый изменяющий SQL попадает в журнал базы <база>_sql.jsonl с состоянием до и после, пользователем и временем. Администратор базы читает его командой journal.

curl -s -H "X-Authorization: $TOKEN" "https://ideav.ru/mydb/journal?JSON&mode=sql"

Журнал хранит и строки изменений, откатившихся по InTransaction.

Внешние вызовы из запроса

Колонка-функция abn_URL и реквизиты запроса URL, URL_POST, URL_HEADER делают HTTP-вызов внешнему сервису на каждую строку выборки, подставляя значения колонок. Ответ читается формулой CURL.<ключ> и тем же «Присвоить» пишется в запись.

Транзакция охватывает только базу: внешний вызов не откатывается, даже если откатились изменения. Обращения к localhost из запросов запрещены.

На практике

Чек-лист процедуры

  • все условия охвата — колонки без «Имени в отчёте»; с именем — только то, что вызывающий вправе выбирать;
  • параметр без значения по умолчанию не отбирает ничего (t102=0 у колонки id), а не всё;
  • ограничитель по вызывающему: [USER_ID] на ссылке на пользователя или условие по [ROLE_ID];
  • поле, которое меняет процедура, у роли — только чтение;
  • роли — READ на запрос; WRITE на таблицы 22 и 28 — только администраторам;
  • опасные изменения — через confirmed=1, EXECUTE — только там, где прогон при каждом вызове задуман;
  • изменения, которые нельзя оставить наполовину, — с InTransaction; массовые — с фильтром «ещё не обработано»;
  • перед вызовом с confirmed=1 посмотрите превью тем же адресом без него.

Грабли

  • пустой результат «Присвоить» удаляет значение, а у колонки главной таблицы — запись;
  • пунктуация внутри значения мультиссылки разрывает его на части: разделитель — любой символ, кроме букв, цифр и пробела;
  • текст в колонке-справочнике с опечаткой создаёт новое значение справочника;
  • EXECUTE включается самим наличием значения реквизита, даже 0; у InTransaction 0 значит «выключено».

Смотрите также

  • справочник API — создание запросов, фильтры, формулы, подзапросы, доступы ролей: /api.html;
  • триггеры кнопок — запуск запроса при создании, изменении и удалении записи: /triggers.html;
  • база знаний, статья 29 — «Запрос как хранимая процедура»: зачем модель и чем она отличается от прав роли.