Чуйгун
Техническая документация

База данных

Схема таблиц 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 push
  • npm 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).

На этой странице