feat: creates backend for the project
This commit is contained in:
142
backend/src/db/migrate.ts
Normal file
142
backend/src/db/migrate.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
10
backend/src/db/pool.ts
Normal file
10
backend/src/db/pool.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Pool } from 'pg';
|
||||
import { config } from '../config';
|
||||
|
||||
export const pool = new Pool({
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
database: config.db.database,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
});
|
||||
Reference in New Issue
Block a user