diff --git a/v2/src/db/schema.js b/v2/src/db/schema.js index 8ee6864..9acae6d 100644 --- a/v2/src/db/schema.js +++ b/v2/src/db/schema.js @@ -1,22 +1,17 @@ // ═══════════════════════════════════════════════════════════════════════════════ -// V2 — инициализация схемы БД + миграции +// V2 — инициализация схемы БД // -// ЭТО: CREATE TABLE IF NOT EXISTS (идемпотентно) + applyMigrations(). +// ЭТО: CREATE TABLE IF NOT EXISTS — безопасный повторный запуск. // ЗАЧЕМ: -// 1. Приложение само создаёт таблицы при старте — не нужны ручные DDL. -// 2. Миграции — отдельные .sql файлы в sql/migrations/, выполняются по порядку. -// 3. Безопасно для production: IF NOT EXISTS + _migrations таблица. +// 1. Приложение само создаёт таблицы при старте. +// 2. Безопасно для production: IF NOT EXISTS. // // ТАБЛИЦЫ: // companies — client_id (W-номер), name, custom_limit // whitelist_entries — CIDR, комментарий, кто создал/изменил/удалил // audit_log — все действия: CREATE/UPDATE/DELETE/RESTORE, с impersonated_by -// _migrations — какие миграции уже выполнены // ═══════════════════════════════════════════════════════════════════════════════ -const fs = require('fs'); -const path = require('path'); - async function ensureSchema(pool) { await pool.query(` -- Компании: W-номер уникален, upsert через ON CONFLICT @@ -53,7 +48,7 @@ async function ensureSchema(pool) { id SERIAL PRIMARY KEY, user_email VARCHAR(255) NOT NULL, company_id INTEGER NOT NULL, - action VARCHAR(32) NOT NULL, + action VARCHAR(32) NOT NULL CHECK (action IN ('CREATE','UPDATE','DELETE','RESTORE')), old_value TEXT, new_value TEXT, entry_id INTEGER, @@ -63,83 +58,32 @@ async function ensureSchema(pool) { CREATE INDEX IF NOT EXISTS idx_audit_log_company_time ON audit_log(company_id, created_at DESC); - - -- Таблица учёта выполненных миграций - CREATE TABLE IF NOT EXISTS _migrations ( - id SERIAL PRIMARY KEY, - filename VARCHAR(255) UNIQUE NOT NULL, - applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); `); + // Миграция: impersonated_by (если колонки ещё нет) + await pool.query(` + ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS impersonated_by VARCHAR(255) DEFAULT NULL + `).catch(() => {}); + + // Миграция: добавить RESTORE в CHECK (если ещё нет) + await pool.query(` + DO $$ + DECLARE + cn text; + BEGIN + SELECT con.conname INTO cn + FROM pg_constraint con + JOIN pg_class rel ON rel.oid = con.conrelid + WHERE rel.relname = 'audit_log' AND con.contype = 'c'; + IF cn IS NOT NULL THEN + EXECUTE 'ALTER TABLE audit_log DROP CONSTRAINT ' || cn; + END IF; + EXECUTE 'ALTER TABLE audit_log ADD CONSTRAINT audit_log_action_check CHECK (action IN (''CREATE'',''UPDATE'',''DELETE'',''RESTORE''))'; + END; + $$; + `).catch(() => {}); + console.log('[db] Schema ensured'); - - // Выполнить неприменённые миграции - await applyMigrations(pool); } -/** - * Применить новые миграции из sql/migrations/. - * Читает все .sql файлы, сортирует по имени, выполняет те, - * которых ещё нет в таблице _migrations. - */ -async function applyMigrations(pool) { - const dir = path.join(__dirname, '..', '..', '..', 'sql', 'migrations'); - if (!fs.existsSync(dir)) { - console.log('[db] No migrations dir, skipping'); - return; - } - - const files = fs.readdirSync(dir) - .filter(f => f.endsWith('.sql')) - .sort(); - - if (files.length === 0) return; - - // Какие уже выполнены - const done = new Set( - (await pool.query('SELECT filename FROM _migrations')).rows.map(r => r.filename) - ); - - for (const file of files) { - if (done.has(file)) continue; - const sql = fs.readFileSync(path.join(dir, file), 'utf8'); - await pool.query(sql); - await pool.query('INSERT INTO _migrations (filename) VALUES ($1)', [file]); - console.log('[db] Migration applied:', file); - } -} - -async function initOldMigrations(pool) { - // Миграции, которые раньше были в ensureSchema() — теперь - // фиксируем их как уже выполненные, чтобы не потерять. - const dir = path.join(__dirname, '..', '..', '..', 'sql', 'migrations'); - if (!fs.existsSync(dir)) return; - const files = fs.readdirSync(dir) - .filter(f => f.endsWith('.sql')) - .sort(); - if (files.length === 0) return; - - // Создать таблицу если ещё нет (на случай первого запуска с миграциями) - await pool.query(` - CREATE TABLE IF NOT EXISTS _migrations ( - id SERIAL PRIMARY KEY, - filename VARCHAR(255) UNIQUE NOT NULL, - applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - `); - - const done = new Set( - (await pool.query('SELECT filename FROM _migrations')).rows.map(r => r.filename) - ); - - for (const file of files) { - if (done.has(file)) continue; - const sql = fs.readFileSync(path.join(dir, file), 'utf8'); - await pool.query(sql); - await pool.query('INSERT INTO _migrations (filename) VALUES ($1)', [file]); - console.log('[db] Migration applied:', file); - } -} - -module.exports = { ensureSchema, applyMigrations, initOldMigrations }; +module.exports = { ensureSchema };