feat(db): перевести хранилище с SQLite на PostgreSQL
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,79 +1,7 @@
|
||||
import assert from "node:assert/strict"
|
||||
import Database from "better-sqlite3"
|
||||
import { normalizeBindingPeer, PeerBindError } from "./peer-bind.js"
|
||||
|
||||
const sqlite = new Database(":memory:")
|
||||
sqlite.pragma("foreign_keys = ON")
|
||||
sqlite.exec(`
|
||||
CREATE TABLE servers (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
host TEXT NOT NULL DEFAULT '127.0.0.1'
|
||||
);
|
||||
CREATE TABLE app_users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
login TEXT NOT NULL UNIQUE,
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
role TEXT NOT NULL DEFAULT 'viewer',
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
avatar TEXT NOT NULL DEFAULT '',
|
||||
last_seen TEXT,
|
||||
sections_json TEXT NOT NULL DEFAULT '[]',
|
||||
servers_json TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE user_interface_bindings (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
server_id INTEGER NOT NULL,
|
||||
interface_name TEXT NOT NULL,
|
||||
interface_type TEXT NOT NULL DEFAULT 'other',
|
||||
peer_public_key TEXT NOT NULL DEFAULT '',
|
||||
peer_name TEXT NOT NULL DEFAULT '',
|
||||
comment TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES app_users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
||||
UNIQUE (server_id, interface_name, peer_public_key)
|
||||
);
|
||||
`)
|
||||
|
||||
sqlite.prepare("INSERT INTO servers (id, name, host) VALUES (1, 'jh', '10.0.0.1')").run()
|
||||
sqlite.prepare("INSERT INTO app_users (id, name, login) VALUES ('u1', 'A', 'a.user')").run()
|
||||
sqlite.prepare("INSERT INTO app_users (id, name, login) VALUES ('u2', 'B', 'b.user')").run()
|
||||
sqlite.prepare(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type)
|
||||
VALUES ('b1', 'u1', 1, 'gre-office', 'gre')
|
||||
`).run()
|
||||
|
||||
assert.throws(
|
||||
() => sqlite.prepare(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type)
|
||||
VALUES ('b2', 'u2', 1, 'gre-office', 'gre')
|
||||
`).run(),
|
||||
/UNIQUE/i,
|
||||
"один интерфейс на сервере — один пользователь",
|
||||
)
|
||||
|
||||
sqlite.prepare(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type, peer_public_key, peer_name)
|
||||
VALUES ('wg1', 'u1', 1, 'wg-server', 'wg', 'peer-key-aaa', 'phone')
|
||||
`).run()
|
||||
sqlite.prepare(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type, peer_public_key, peer_name)
|
||||
VALUES ('wg2', 'u2', 1, 'wg-server', 'wg', 'peer-key-bbb', 'laptop')
|
||||
`).run()
|
||||
assert.throws(
|
||||
() => sqlite.prepare(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type, peer_public_key)
|
||||
VALUES ('wg3', 'u2', 1, 'wg-server', 'wg', 'peer-key-aaa')
|
||||
`).run(),
|
||||
/UNIQUE/i,
|
||||
"один пир — один пользователь",
|
||||
)
|
||||
import { withPgOrSkip } from "../../test/pg.js"
|
||||
import { dbQuery } from "../../db/index.js"
|
||||
|
||||
assert.throws(
|
||||
() => normalizeBindingPeer("wg", ""),
|
||||
@@ -83,8 +11,64 @@ assert.throws(
|
||||
assert.equal(normalizeBindingPeer("ether", "ignored"), "")
|
||||
assert.equal(normalizeBindingPeer("wg", " abc "), "abc")
|
||||
|
||||
sqlite.prepare("DELETE FROM app_users WHERE id = 'u1'").run()
|
||||
const leftover = sqlite.prepare("SELECT COUNT(*) AS n FROM user_interface_bindings").get() as { n: number }
|
||||
assert.equal(leftover.n, 1, "каскад: привязки u1 удаляются, пир u2 остаётся")
|
||||
if (!(await withPgOrSkip())) {
|
||||
console.log("users bindings unique+cascade tests skip")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
await dbQuery(`
|
||||
INSERT INTO servers (id, name, host) VALUES (91001, 'jh-bind-test', '10.0.0.1')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`)
|
||||
await dbQuery(`
|
||||
INSERT INTO app_users (id, name, login)
|
||||
VALUES ('u-bind-1', 'A', 'a.bind.test'), ('u-bind-2', 'B', 'b.bind.test')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`)
|
||||
await dbQuery(`DELETE FROM user_interface_bindings WHERE server_id = 91001`)
|
||||
await dbQuery(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type)
|
||||
VALUES ('b-bind-1', 'u-bind-1', 91001, 'gre-office', 'gre')
|
||||
`)
|
||||
|
||||
let uniqueIface = false
|
||||
try {
|
||||
await dbQuery(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type)
|
||||
VALUES ('b-bind-2', 'u-bind-2', 91001, 'gre-office', 'gre')
|
||||
`)
|
||||
} catch {
|
||||
uniqueIface = true
|
||||
}
|
||||
assert.equal(uniqueIface, true, "один интерфейс на сервере — один пользователь")
|
||||
|
||||
await dbQuery(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type, peer_public_key, peer_name)
|
||||
VALUES ('wg-bind-1', 'u-bind-1', 91001, 'wg-server', 'wg', 'peer-key-aaa', 'phone')
|
||||
`)
|
||||
await dbQuery(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type, peer_public_key, peer_name)
|
||||
VALUES ('wg-bind-2', 'u-bind-2', 91001, 'wg-server', 'wg', 'peer-key-bbb', 'laptop')
|
||||
`)
|
||||
|
||||
let uniquePeer = false
|
||||
try {
|
||||
await dbQuery(`
|
||||
INSERT INTO user_interface_bindings (id, user_id, server_id, interface_name, interface_type, peer_public_key)
|
||||
VALUES ('wg-bind-3', 'u-bind-2', 91001, 'wg-server', 'wg', 'peer-key-aaa')
|
||||
`)
|
||||
} catch {
|
||||
uniquePeer = true
|
||||
}
|
||||
assert.equal(uniquePeer, true, "один пир — один пользователь")
|
||||
|
||||
await dbQuery(`DELETE FROM app_users WHERE id = 'u-bind-1'`)
|
||||
const leftover = await dbQuery<{ n: number }>(
|
||||
`SELECT COUNT(*)::int AS n FROM user_interface_bindings WHERE server_id = 91001`,
|
||||
)
|
||||
assert.equal(leftover.rows[0]?.n, 1, "каскад: привязки u1 удаляются, пир u2 остаётся")
|
||||
|
||||
await dbQuery(`DELETE FROM app_users WHERE id IN ('u-bind-1', 'u-bind-2')`)
|
||||
await dbQuery(`DELETE FROM servers WHERE id = 91001`)
|
||||
|
||||
console.log("users bindings unique+cascade tests ok")
|
||||
|
||||
Reference in New Issue
Block a user