refactor(arch): split database.js into migrations + connection module (#57)

- database.js: 292→42 lines (connection + async helpers only)
- 001_initial_schema.js: 7 CREATE TABLE statements in transaction
- 002_add_columns.js: 5 ALTER TABLE checks with checkColumnExists
- 003_add_indexes.js: 6 CREATE INDEX statements
- runner.js: versioned migration runner with _meta table
- index.js: calls runMigrations() + cleanUpInvalidForeignKeys()
- ALLOWED_TABLES whitelist preserved in runner.js
- Schema version tracked in _meta table for idempotent runs
This commit is contained in:
NW
2026-06-17 22:28:11 +01:00
parent 2e8b6b5659
commit 4b8144ac40
6 changed files with 206 additions and 262 deletions

View File

@@ -0,0 +1,92 @@
export default async function migration001(db) {
await db.runAsync('BEGIN TRANSACTION');
await db.runAsync(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id TEXT UNIQUE NOT NULL,
username TEXT,
country TEXT,
city TEXT,
district TEXT,
status INTEGER DEFAULT 0,
total_balance REAL DEFAULT 0,
bonus_balance REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
await db.runAsync(`CREATE TABLE IF NOT EXISTS crypto_wallets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
wallet_type TEXT NOT NULL,
address TEXT NOT NULL,
derivation_path TEXT NOT NULL,
mnemonic TEXT NOT NULL,
balance REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, wallet_type)
)`);
await db.runAsync(`CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
wallet_type TEXT NOT NULL,
tx_hash TEXT NOT NULL,
amount REAL NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`);
await db.runAsync(`CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
private_data TEXT,
price REAL NOT NULL CHECK (price > 0),
quantity_in_stock INTEGER DEFAULT 0 CHECK (quantity_in_stock >= 0),
photo_url TEXT,
hidden_photo_url TEXT,
hidden_coordinates TEXT,
hidden_description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
)`);
await db.runAsync(`CREATE TABLE IF NOT EXISTS purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
wallet_type TEXT NOT NULL,
tx_hash TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
total_price REAL NOT NULL CHECK (total_price > 0),
purchase_date DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'pending',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
)`);
await db.runAsync(`CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
country TEXT NOT NULL,
city TEXT NOT NULL,
district TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(country, city, district)
)`);
await db.runAsync(`CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
UNIQUE(location_id, name)
)`);
await db.runAsync('COMMIT');
console.log('Migration 001: Initial schema created');
}

View File

@@ -0,0 +1,33 @@
export default async function migration002(db, checkColumnExists) {
const balanceExists = await checkColumnExists('crypto_wallets', 'balance');
if (!balanceExists) {
await db.runAsync(`ALTER TABLE crypto_wallets ADD COLUMN balance REAL DEFAULT 0`);
console.log('Migration 002: Column balance added to crypto_wallets');
}
const userIdExists = await checkColumnExists('transactions', 'user_id');
if (!userIdExists) {
await db.runAsync(`ALTER TABLE transactions ADD COLUMN user_id INTEGER NOT NULL`);
console.log('Migration 002: Column user_id added to transactions');
}
const walletTypeExists = await checkColumnExists('transactions', 'wallet_type');
if (!walletTypeExists) {
await db.runAsync(`ALTER TABLE transactions ADD COLUMN wallet_type TEXT NOT NULL`);
console.log('Migration 002: Column wallet_type added to transactions');
}
const txHashExists = await checkColumnExists('transactions', 'tx_hash');
if (!txHashExists) {
await db.runAsync(`ALTER TABLE transactions ADD COLUMN tx_hash TEXT NOT NULL`);
console.log('Migration 002: Column tx_hash added to transactions');
}
const statusExists = await checkColumnExists('purchases', 'status');
if (!statusExists) {
await db.runAsync(`ALTER TABLE purchases ADD COLUMN status TEXT DEFAULT 'pending'`);
console.log('Migration 002: Column status added to purchases');
}
console.log('Migration 002: Column additions complete');
}

View File

@@ -0,0 +1,9 @@
export default async function migration003(db) {
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id)`);
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_crypto_wallets_user_type ON crypto_wallets(user_id, wallet_type)`);
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_transactions_user ON transactions(user_id)`);
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_purchases_user_product ON purchases(user_id, product_id)`);
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_purchases_status ON purchases(status)`);
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_products_location_category ON products(location_id, category_id)`);
console.log('Migration 003: Indexes created');
}

56
src/migrations/runner.js Normal file
View File

@@ -0,0 +1,56 @@
import db from '../config/database.js';
const ALLOWED_TABLES = new Set([
'users', 'crypto_wallets', 'transactions', 'products',
'purchases', 'locations', 'categories'
]);
export const checkColumnExists = async (tableName, columnName) => {
if (!ALLOWED_TABLES.has(tableName)) {
throw new Error(`Invalid table name: ${tableName}`);
}
try {
const result = await db.allAsync(`PRAGMA table_info(${tableName})`);
return result.some(column => column.name === columnName);
} catch (error) {
console.error(`Error checking column ${columnName} in table ${tableName}:`, error);
return false;
}
};
export const cleanUpInvalidForeignKeys = async () => {
try {
await db.runAsync(`DELETE FROM crypto_wallets WHERE user_id NOT IN (SELECT id FROM users)`);
console.log('Cleaned up invalid foreign key references in crypto_wallets table');
} catch (error) {
console.error('Error cleaning up invalid foreign key references:', error);
}
};
export async function runMigrations() {
await db.runAsync(`CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)`);
const row = await db.getAsync(`SELECT value FROM _meta WHERE key = 'schema_version'`);
const currentVersion = row ? parseInt(row.value, 10) : 0;
const migrations = [
(await import('./001_initial_schema.js')).default,
(await import('./002_add_columns.js')).default,
(await import('./003_add_indexes.js')).default,
];
for (let i = currentVersion; i < migrations.length; i++) {
console.log(`Running migration ${i + 1}/${migrations.length}...`);
if (i === 1) {
await migrations[i](db, checkColumnExists);
} else {
await migrations[i](db);
}
await db.runAsync(
`INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)`,
[String(i + 1)]
);
}
console.log(`Migrations complete. Schema version: ${migrations.length}`);
}