feat(tradein/db): seed существующих юзеров + ASCII-CHECK на username (#2557) #2564

Merged
lekss361 merged 2 commits from feat/tradein-users-seed into main 2026-07-30 19:17:35 +00:00
Showing only changes of commit f915e1ee4d - Show all commits

View file

@ -22,8 +22,9 @@
-- --
-- WHAT: -- WHAT:
-- 1. ASCII-CHECK: tradein_users_username_ascii_ck CHECK (username ~ '^[A-Za-z0-9._-]{3,64}$'). -- 1. ASCII-CHECK: tradein_users_username_ascii_ck CHECK (username ~ '^[A-Za-z0-9._-]{3,64}$').
-- Добавлен ДО seed-инсертов (порядок важен: constraint должен успеть провалидировать -- Добавлен ДО seed-инсертов ниже для читаемости файла (CHECK — immediate constraint,
-- вставляемые строки, а не просто существовать после них). -- Postgres валидирует им и ROW-строки транзакции независимо от того, в каком месте
-- файла он объявлен относительно INSERT, так что порядок сам по себе не критичен).
-- 2. Seed — org-карта, утверждённая владельцем продукта (2026-07-30): -- 2. Seed — org-карта, утверждённая владельцем продукта (2026-07-30):
-- admin role=admin, manager_id=NULL, is_active=true (владелец) -- admin role=admin, manager_id=NULL, is_active=true (владелец)
-- kopylov role=manager, manager_id=NULL, is_active=true (отдельный клиент) -- kopylov role=manager, manager_id=NULL, is_active=true (отдельный клиент)
@ -46,16 +47,27 @@
-- `ADD CONSTRAINT IF NOT EXISTS` для CHECK) — паттерн из -- `ADD CONSTRAINT IF NOT EXISTS` для CHECK) — паттерн из
-- 189_account_estimate_usage_nonnegative.sql. -- 189_account_estimate_usage_nonnegative.sql.
-- - INSERT ... ON CONFLICT (username) DO UPDATE, но НЕ безусловно: password_hash, -- - INSERT ... ON CONFLICT (username) DO UPDATE, но НЕ безусловно: password_hash,
-- is_active, display_name, org_name, email защищены COALESCE(текущее, EXCLUDED) — -- manager_id, display_name, org_name, email защищены COALESCE(текущее, EXCLUDED) —
-- если админ уже проставил пароль / поменял is_active / display_name вручную через -- если админ уже проставил пароль / назначил manager_id (team-API #2563 пишет
-- team-UI (#2556) между двумя прогонами этого файла (например ручной re-apply при -- manager_id при создании сотрудника менеджером) / поменял display_name вручную
-- recovery — обычный auto-apply тречит filename в _schema_migrations и не запускает -- через team-UI (#2556) между двумя прогонами этого файла (например ручной re-apply
-- файл дважды на одном окружении, но scratch/staging БД такого трекинга не имеют), -- при recovery — обычный auto-apply тречит filename в _schema_migrations и не
-- повторный прогон НЕ должен затереть это состояние NULL-ом / seed-дефолтом. -- запускает файл дважды на одном окружении, но scratch/staging БД такого
-- role/manager_id намеренно синкаются с EXCLUDED (не защищены) — это и есть источник -- трекинга не имеют), повторный прогон НЕ должен затереть это состояние NULL-ом /
-- истины org-карты из этой миграции; если владелец продукта поправит эту таблицу -- seed-дефолтом. Deep-review #2564 нашёл это живым багом: manager_id, назначенный
-- новой миграцией поверх, DO UPDATE-ветка должна донести исправление, а не заморозить -- через #2563, тихо обнулялся повторным прогоном сида — employee выпадал из
-- первый прогон навсегда. -- `_LIST_EMPLOYEES_BY_MANAGER_SQL`, менеджер переставал видеть его в дашборде.
-- role намеренно синкается с EXCLUDED (не защищён) — это и есть источник истины
-- org-карты из этой миграции; если владелец продукта поправит эту таблицу новой
-- миграцией поверх, DO UPDATE-ветка должна донести исправление роли, а не
-- заморозить первый прогон навсегда.
-- - is_active НАМЕРЕННО отсутствует в SET (не COALESCE — колонка NOT NULL DEFAULT
-- true, COALESCE(NOT NULL, x) никогда не берёт x, это была бы мёртвая, вводящая в
-- заблуждение симметрия с password_hash/manager_id, deep-review #2564 medium).
-- Открытие/закрытие доступа (is_active) — решение владельца продукта, принимается
-- через UI (#2556), НЕ повторным прогоном этого seed-файла: после первой вставки
-- колонка сознательно «замораживается» на текущем значении в БД, seed её больше
-- не трогает.
-- --
-- Dependencies: 192_tradein_users_auth.sql (создаёт tradein_users, tradein_sessions). -- Dependencies: 192_tradein_users_auth.sql (создаёт tradein_users, tradein_sessions).
-- Deploy order: после 192 (Foundation). Backend auth-код (login/password-verify) и -- Deploy order: после 192 (Foundation). Backend auth-код (login/password-verify) и
@ -64,7 +76,7 @@
BEGIN; BEGIN;
-- Часть 1: ASCII-CHECK — должен применяться ДО вставки seed-строк ниже. -- Часть 1: ASCII-CHECK (immediate constraint — валидирует и вставляемые ниже строки).
DO $$ DO $$
BEGIN BEGIN
IF NOT EXISTS ( IF NOT EXISTS (
@ -100,13 +112,16 @@ VALUES
('user9', NULL, 'employee', NULL, NULL, NULL, NULL, true), ('user9', NULL, 'employee', NULL, NULL, NULL, NULL, true),
('user10', NULL, 'employee', NULL, NULL, NULL, NULL, true) ('user10', NULL, 'employee', NULL, NULL, NULL, NULL, true)
ON CONFLICT (username) DO UPDATE SET ON CONFLICT (username) DO UPDATE SET
role = EXCLUDED.role, role = EXCLUDED.role,
manager_id = EXCLUDED.manager_id, -- manager_id защищён COALESCE: team-API (#2563) пишет manager_id при назначении
-- сотрудника менеджером, повторный прогон seed'а не должен тихо обнулять эту связь.
manager_id = COALESCE(tradein_users.manager_id, EXCLUDED.manager_id),
password_hash = COALESCE(tradein_users.password_hash, EXCLUDED.password_hash), password_hash = COALESCE(tradein_users.password_hash, EXCLUDED.password_hash),
is_active = COALESCE(tradein_users.is_active, EXCLUDED.is_active), display_name = COALESCE(tradein_users.display_name, EXCLUDED.display_name),
display_name = COALESCE(tradein_users.display_name, EXCLUDED.display_name), org_name = COALESCE(tradein_users.org_name, EXCLUDED.org_name),
org_name = COALESCE(tradein_users.org_name, EXCLUDED.org_name), email = COALESCE(tradein_users.email, EXCLUDED.email),
email = COALESCE(tradein_users.email, EXCLUDED.email), -- is_active НЕ в SET: NOT NULL DEFAULT true колонка, COALESCE был бы мёртвым кодом
updated_at = now(); -- (см. IDEMPOTENCY выше) — open/close доступа решается через UI (#2556), не seed'ом.
updated_at = now();
COMMIT; COMMIT;