База данных
Схема таблиц Postgres/Supabase, дисциплина миграций forward-only, RPC с SECURITY DEFINER, RLS и Realtime-синхронизация.
Основные таблицы (Postgres / Supabase)
| Таблица | Назначение |
|---|---|
categories | категории меню (slug, name_ru/en, sort_order, is_active) |
dishes | блюда (category_id FK, price_som int без копеек, weight_g, photo_url в Storage, is_available — стоп-лист) |
tables | столы (number UNIQUE, zone, seats, qr_token UNIQUE от подмены, assigned_waiter_id — мягкая привязка официанта, #36) |
orders | заказы (order_number serial, source dine_in/pickup/delivery, table_id, customer_*, status, total_som, таймстемпы accepted/ready/closed, client_order_id/idempotency) |
order_items | позиции (dish_name_snapshot + price_snapshot на момент заказа, quantity, comment — per-item заметка повару, #55) |
profiles | профили (FK auth.users, role) |
shifts | смены для отчётности |
Миграции (forward-only)
51+ миграций применены на prod (актуально 055). Идемпотентны (if not exists), применяются в числовом порядке.
Миграции только вперёд. In-place правку уже применённой миграции запрещено — рассинхронизирует историю prod (#55). Изменение RPC — через новую миграцию CREATE OR REPLACE FUNCTION.
Пример дисциплины (#55): колонка order_items.comment добавлена миграцией 054, приём per-item comment — отдельной 055 через CREATE OR REPLACE (а не правкой уже применённой 010). CHECK length ≤ 200 защищает термочек от переполнения.
Команды:
supabase db pushnpm run db:test— тесты SQL-миграций- seed:
psql "$DATABASE_URL" -f db/seed/menu-seed.sql
RPC с SECURITY DEFINER
Все мутации только через RPC (#3). Прямые INSERT / UPDATE запрещены.
- Публичные RPC (anon EXECUTE):
get_menu,validate_table_qr,create_order,get_menu_en_fallback. - Мутации:
create_order,update_order_status,cancel_order.update_order_status(миграция 015) ре-проверяет роль на сервере. current_is_order_staff()(миграция 015) — единый источник истины «кто staff», переиспользуется во всех mutation-RPC.create_orderвалидирует стоп-лист: недоступное блюдо →INVALID_DISH(#53).
RLS (Row Level Security)
Инвариант: у каждой таблицы в public включён RLS. Статически проверяется db/__tests__/rls-coverage.test.mjs на каждом CI-билде.
Гранты минимальны:
anon— SELECT только наcategories/dishes/tables+ EXECUTE на 4 публичных RPC.authenticated— SELECT / UPDATE наordersпод staff-политиками.service_role— только у print-agent и дашборда.
Anti-self-promotion: profiles_self_update WITH CHECK пинит role к текущему — waiter не поднимет себя до admin прямым UPDATE.
Стоп-лист (#53, migration 053): is_available=false видно всем с меткой «Нет в наличии» (из anon RLS снято только is_available, оставлен is_active). Прячется не блюдо, а возможность заказа.
Realtime
Подписки на orders для кассира, кухни и print-agent (фильтр по status).
Стоп-лист синхронизируется realtime (#54, migration 052: publication + replica identity full): подписка на public.dishes, клиент применяет applyDishAvailability + poll-backstop 15s + revalidateTag('menu'). Цель: повар пометил «кончилось» → у всех менее 2с.
Push-уведомления: pg_net-триггер в миграции (046, #27) вместо ручного Dashboard-вебхука. Два отдельных триггера — на INSERT и UPDATE OF status (049, #34), т.к. Postgres запрещает WHEN OLD в комбинированном INSERT-OR-UPDATE триггере (42P17).