Audit log, soft-delete, версионирование
Safety layer для DM-as-demiurge: вместо ограничения прав — следы.
event.deleted_atвместоDELETE. Версии контентных сущностей. Таблицаdm_audit_log. Сейчас в проде — hard DELETE на чувствительных таблицах, никакого версионирования.
Философия: следы, не guardrails
DM может всё. Это принцип продукта: concepts/dm-as-demiurge.md.
Safety обеспечивается не запретом операций, а воспроизводимостью истории.
Если что-то «исчезло» — это можно найти в логе. Если что-то «изменилось» —
старая версия сохранена.
Единственные физические ограничения DM — технические, не логические:
DM не может уронить базу, удалить чужую кампанию или писать в auth.users.
Всё остальное — разрешено.
Soft-delete: event.deleted_at
Целевая модель: ни одна строка из events не удаляется физически.
-- В целевой таблице events (см. generic-events-table.md):
deleted_at timestamptz -- null = живой; not null = удалён
-- Все продуктовые запросы:
where deleted_at is null
-- DM «удаляет» событие:
update events set deleted_at = now() where id = $1;
-- DM «восстанавливает»:
update events set deleted_at = null where id = $1;
Аналогично — для других таблиц, где hard DELETE сейчас применяется к значимым
данным (categories, узлы-ноды при удалении DM'ом).
В текущем проде categories.is_deleted boolean (миграция 034) — частичный
прецедент этого паттерна, но не через deleted_at.
Версионирование контент-сущностей
Каждая content-сущность (предмет, монстр, спелл) получает version: int.
Правка не перезаписывает — создаёт новую версию; старые остаются.
Ссылки из событий указывают на конкретную (item_id, item_version).
-- Пример для items (целевое состояние):
alter table items add column version int not null default 1;
alter table items add column is_current bool not null default true;
alter table items add column previous_id uuid references items(id);
-- «Редактировать» = создать новую строку:
-- new_item = {…изменённые поля…, version = old.version + 1, is_current = true}
-- update items set is_current = false where id = old_id
-- Событие: {item_id: new_item.id, item_version: new_item.version}
Восстановление к версии N: скопировать строку с version=N как новую текущую.
Когда это не нужно: ноды graph (title/fields) — они правятся in-place,
история редактора не хранится. Версионирование применяется к
контент-сущностям, на которые ссылаются events.
Таблица dm_audit_log
create table dm_audit_log (
id uuid primary key default gen_random_uuid(),
campaign_id uuid not null references campaigns(id) on delete cascade,
action_type text not null,
-- 'event_delete', 'event_restore', 'content_edit', 'npc_override',
-- 'location_edit', 'member_kick', 'loop_reset', …
target_id uuid, -- id затронутой сущности
target_table text, -- 'events', 'items', 'npc_groups', …
before_state jsonb, -- состояние до (null если создание)
after_state jsonb, -- состояние после (null если удаление)
author_user_id uuid not null references auth.users(id) on delete set null,
created_at timestamptz not null default now()
);
RLS: dm_audit_log читается только участниками с ролью owner кампании.
DM-аккаунты (роль dm) видят только собственные записи (фильтр author_user_id = auth.uid()).
Пишется через server action (или триггер) при каждой DM-операции, которая
меняет events, контент-сущности или члены кампании.
Что DM не может
Это не ограничения прав в RLS — это физические барьеры инфраструктуры:
- DROP/TRUNCATE любой таблицы — роль
authenticatedв Supabase не имеет таких привилегий; толькоsupabase_adminчерез Studio (SSH-туннель). - Удалить чужую кампанию —
campaign_idв RLS всегда проверяется черезis_member(campaign_id); нечлен кампании не может трогать её данные. - Писать в
auth.users— таблица в схемеauth, RLS и ролиauthenticatedтуда не достают. - Читать другие чужие кампании — RLS-функция
is_memberпроверяет таблицуcampaign_members.
UI: история правок
Когда dm_audit_log наполнится, DM-интерфейс покажет:
- Историю правок конкретной ноды/события (фильтр по
target_id). - Возможность «откатить» к версии N (для контент-сущностей с версионированием).
- Лог удалённых событий с кнопкой восстановления.
До того момента — dm_audit_log существует как технический лог без
продуктового UI.
Принцип DM-as-demiurge:
concepts/dm-as-demiurge.md. Универсальный лог:generic-events-table.md.