feat: creates backend for the project

This commit is contained in:
vakabunga
2026-03-02 00:32:37 +03:00
parent 9d12702688
commit 4d67636633
24 changed files with 1735 additions and 0 deletions

142
backend/src/db/migrate.ts Normal file
View File

@@ -0,0 +1,142 @@
import { pool } from './pool';
const migrations: { name: string; sql: string }[] = [
{
name: '001_create_tables',
sql: `
CREATE TABLE IF NOT EXISTS accounts (
id BIGSERIAL PRIMARY KEY,
bank TEXT NOT NULL,
account_number TEXT NOT NULL,
currency TEXT NOT NULL,
alias TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS ux_accounts_bank_number
ON accounts(bank, account_number);
CREATE TABLE IF NOT EXISTS categories (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
ALTER TABLE categories DROP CONSTRAINT IF EXISTS chk_categories_type;
ALTER TABLE categories
ADD CONSTRAINT chk_categories_type
CHECK (type IN ('expense', 'income', 'transfer'));
CREATE TABLE IF NOT EXISTS transactions (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
operation_at TIMESTAMPTZ NOT NULL,
amount_signed BIGINT NOT NULL,
commission BIGINT NOT NULL,
description TEXT NOT NULL,
direction TEXT NOT NULL,
fingerprint TEXT NOT NULL,
category_id BIGINT REFERENCES categories(id),
is_category_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
comment TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS ux_transactions_account_fingerprint
ON transactions(account_id, fingerprint);
ALTER TABLE transactions DROP CONSTRAINT IF EXISTS chk_transactions_direction;
ALTER TABLE transactions
ADD CONSTRAINT chk_transactions_direction
CHECK (direction IN ('income', 'expense', 'transfer'));
CREATE TABLE IF NOT EXISTS category_rules (
id BIGSERIAL PRIMARY KEY,
pattern TEXT NOT NULL,
match_type TEXT NOT NULL,
category_id BIGINT NOT NULL REFERENCES categories(id),
priority INT NOT NULL DEFAULT 0,
requires_confirmation BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
`,
},
{
name: '002_seed_categories',
sql: `
INSERT INTO categories (name, type) VALUES
('Продукты', 'expense'),
('Авто', 'expense'),
('Здоровье', 'expense'),
('Арчи', 'expense'),
('ЖКХ', 'expense'),
('Дом', 'expense'),
('Проезд', 'expense'),
('Одежда', 'expense'),
('Химия', 'expense'),
('Косметика', 'expense'),
('Инвестиции', 'transfer'),
('Развлечения', 'expense'),
('Общепит', 'expense'),
('Штрафы', 'expense'),
('Налоги', 'expense'),
('Подписки', 'expense'),
('Перевод', 'transfer'),
('Наличные', 'expense'),
('Подарки', 'expense'),
('Спорт', 'expense'),
('Отпуск', 'expense'),
('Техника', 'expense'),
('Поступления', 'income')
ON CONFLICT DO NOTHING;
`,
},
];
export async function runMigrations(): Promise<void> {
await pool.query(`
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
for (const m of migrations) {
const { rows } = await pool.query(
'SELECT 1 FROM _migrations WHERE name = $1',
[m.name],
);
if (rows.length > 0) continue;
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(m.sql);
await client.query('INSERT INTO _migrations (name) VALUES ($1)', [m.name]);
await client.query('COMMIT');
console.log(`Migration applied: ${m.name}`);
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
}
if (require.main === module) {
runMigrations()
.then(() => {
console.log('All migrations applied');
process.exit(0);
})
.catch((err) => {
console.error('Migration failed:', err);
process.exit(1);
});
}