Откат миграций: старый ensureSchema без applyMigrations
This commit is contained in:
+29
-85
@@ -1,22 +1,17 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// V2 — инициализация схемы БД + миграции
|
// V2 — инициализация схемы БД
|
||||||
//
|
//
|
||||||
// ЭТО: CREATE TABLE IF NOT EXISTS (идемпотентно) + applyMigrations().
|
// ЭТО: CREATE TABLE IF NOT EXISTS — безопасный повторный запуск.
|
||||||
// ЗАЧЕМ:
|
// ЗАЧЕМ:
|
||||||
// 1. Приложение само создаёт таблицы при старте — не нужны ручные DDL.
|
// 1. Приложение само создаёт таблицы при старте.
|
||||||
// 2. Миграции — отдельные .sql файлы в sql/migrations/, выполняются по порядку.
|
// 2. Безопасно для production: IF NOT EXISTS.
|
||||||
// 3. Безопасно для production: IF NOT EXISTS + _migrations таблица.
|
|
||||||
//
|
//
|
||||||
// ТАБЛИЦЫ:
|
// ТАБЛИЦЫ:
|
||||||
// companies — client_id (W-номер), name, custom_limit
|
// companies — client_id (W-номер), name, custom_limit
|
||||||
// whitelist_entries — CIDR, комментарий, кто создал/изменил/удалил
|
// whitelist_entries — CIDR, комментарий, кто создал/изменил/удалил
|
||||||
// audit_log — все действия: CREATE/UPDATE/DELETE/RESTORE, с impersonated_by
|
// audit_log — все действия: CREATE/UPDATE/DELETE/RESTORE, с impersonated_by
|
||||||
// _migrations — какие миграции уже выполнены
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
async function ensureSchema(pool) {
|
async function ensureSchema(pool) {
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
-- Компании: W-номер уникален, upsert через ON CONFLICT
|
-- Компании: W-номер уникален, upsert через ON CONFLICT
|
||||||
@@ -53,7 +48,7 @@ async function ensureSchema(pool) {
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_email VARCHAR(255) NOT NULL,
|
user_email VARCHAR(255) NOT NULL,
|
||||||
company_id INTEGER 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,
|
old_value TEXT,
|
||||||
new_value TEXT,
|
new_value TEXT,
|
||||||
entry_id INTEGER,
|
entry_id INTEGER,
|
||||||
@@ -63,83 +58,32 @@ async function ensureSchema(pool) {
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_audit_log_company_time
|
CREATE INDEX IF NOT EXISTS idx_audit_log_company_time
|
||||||
ON audit_log(company_id, created_at DESC);
|
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');
|
console.log('[db] Schema ensured');
|
||||||
|
|
||||||
// Выполнить неприменённые миграции
|
|
||||||
await applyMigrations(pool);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
module.exports = { ensureSchema };
|
||||||
* Применить новые миграции из 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 };
|
|
||||||
|
|||||||
Reference in New Issue
Block a user