From 4b8144ac404a55e9c5744d9e59740f2a8b26cfb9 Mon Sep 17 00:00:00 2001 From: NW Date: Wed, 17 Jun 2026 22:28:11 +0100 Subject: [PATCH] refactor(arch): split database.js into migrations + connection module (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/config/database.js | 274 ++------------------------- src/index.js | 4 + src/migrations/001_initial_schema.js | 92 +++++++++ src/migrations/002_add_columns.js | 33 ++++ src/migrations/003_add_indexes.js | 9 + src/migrations/runner.js | 56 ++++++ 6 files changed, 206 insertions(+), 262 deletions(-) create mode 100644 src/migrations/001_initial_schema.js create mode 100644 src/migrations/002_add_columns.js create mode 100644 src/migrations/003_add_indexes.js create mode 100644 src/migrations/runner.js diff --git a/src/config/database.js b/src/config/database.js index 955d2ef..a3ac514 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -1,19 +1,7 @@ -// database.js - import sqlite3 from 'sqlite3'; -import { promisify } from 'util'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -const __dirname = dirname(fileURLToPath(import.meta.url)); const DB_PATH = new URL('../../db/shop.db', import.meta.url).pathname; -const ALLOWED_TABLES = new Set([ - 'users', 'crypto_wallets', 'transactions', 'products', - 'purchases', 'locations', 'categories' -]); - -// Create database with verbose mode for better error reporting const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_CREATE | sqlite3.OPEN_READWRITE, (err) => { if (err) { console.error('Database connection error:', err); @@ -22,271 +10,33 @@ const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_CREATE | sqlite3.OPEN_READ console.log('Connected to SQLite database'); }); -// Enable foreign keys db.run('PRAGMA foreign_keys = ON'); -// Promisify database operations -const runAsync = (sql, params = []) => { - return new Promise((resolve, reject) => { - db.run(sql, params, function(err) { - if (err) reject(err); - else resolve(this); - }); +db.runAsync = (sql, params = []) => + new Promise((resolve, reject) => { + db.run(sql, params, function(err) { if (err) reject(err); else resolve(this); }); }); -}; -const allAsync = (sql, params = []) => { - return new Promise((resolve, reject) => { - db.all(sql, params, (err, rows) => { - if (err) reject(err); - else resolve(rows); - }); +db.allAsync = (sql, params = []) => + new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { if (err) reject(err); else resolve(rows); }); }); -}; -const getAsync = (sql, params = []) => { - return new Promise((resolve, reject) => { - db.get(sql, params, (err, row) => { - if (err) reject(err); - else resolve(row); - }); +db.getAsync = (sql, params = []) => + new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { if (err) reject(err); else resolve(row); }); }); -}; -// Attach async methods to db object -db.runAsync = runAsync; -db.allAsync = allAsync; -db.getAsync = getAsync; - -// Function to check if a column exists in a table -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; - } -}; - -// Function to clean up invalid foreign key references -const cleanUpInvalidForeignKeys = async () => { - try { - // Clean up invalid foreign key references in crypto_wallets table - 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); - } -}; - -// Initialize database tables -const initDb = async () => { - try { - // Begin transaction for table creation - await db.runAsync('BEGIN TRANSACTION'); - - // Create users table - 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 - ) - `); - - // Create crypto_wallets table - 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) - ) - `); - - // Check if balance column exists in crypto_wallets table - const balanceExists = await checkColumnExists('crypto_wallets', 'balance'); - if (!balanceExists) { - await db.runAsync(` - ALTER TABLE crypto_wallets - ADD COLUMN balance REAL DEFAULT 0 - `); - console.log('Column balance added to crypto_wallets table'); - } - - // Create transactions table - 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 - ) - `); - - // Check if user_id column exists in transactions table - const user_idExists = await checkColumnExists('transactions', 'user_id'); - if (!user_idExists) { - await db.runAsync(` - ALTER TABLE transactions - ADD COLUMN user_id INTEGER NOT NULL - `); - console.log('Column user_id added to transactions table'); - } - - // Check if wallet_type column exists in transactions table - const wallet_typeExists = await checkColumnExists('transactions', 'wallet_type'); - if (!wallet_typeExists) { - await db.runAsync(` - ALTER TABLE transactions - ADD COLUMN wallet_type TEXT NOT NULL - `); - console.log('Column wallet_type added to transactions table'); - } - - // Check if tx_hash column exists in transactions table - const tx_hashExists = await checkColumnExists('transactions', 'tx_hash'); - if (!tx_hashExists) { - await db.runAsync(` - ALTER TABLE transactions - ADD COLUMN tx_hash TEXT NOT NULL - `); - console.log('Column tx_hash added to transactions table'); - } - - // Create products table - 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 - ) - `); - - // Create purchases table - 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 - ) - `); - - // Проверка наличия поля status в таблице purchases - const statusExists = await checkColumnExists('purchases', 'status'); - if (!statusExists) { - await db.runAsync(` - ALTER TABLE purchases - ADD COLUMN status TEXT DEFAULT 'pending' - `); - console.log('Column status added to purchases table'); - } - - // Create locations table - 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) - ) - `); - - // Create categories table - 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) - ) - `); - - // Commit transaction - await db.runAsync('COMMIT'); - console.log('Database tables initialized successfully'); - } catch (error) { - // Rollback transaction on error - await db.runAsync('ROLLBACK'); - console.error('Error initializing database tables:', error); - throw error; - } -}; - -// Initialize the database -(async () => { - await initDb(); - await cleanUpInvalidForeignKeys(); -})().catch(error => { - console.error('Database initialization failed:', error); - process.exit(1); -}); - -// Handle database errors db.on('error', (err) => { console.error('Database error:', err); }); -// Handle process termination process.on('SIGINT', () => { db.close((err) => { - if (err) { - console.error('Error closing database:', err); - } else { - console.log('Database connection closed'); - } + if (err) console.error('Error closing database:', err); + else console.log('Database connection closed'); process.exit(err ? 1 : 0); }); }); -export default db; \ No newline at end of file +export default db; diff --git a/src/index.js b/src/index.js index 0a89938..20d8147 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,10 @@ +import { runMigrations, cleanUpInvalidForeignKeys } from './migrations/runner.js'; import adminUserHandler from './handlers/adminHandlers/adminUserHandler.js'; import ErrorHandler from './utils/errorHandler.js'; import bot from "./context/bot.js"; + +await runMigrations(); +await cleanUpInvalidForeignKeys(); import userHandler from "./handlers/userHandlers/userHandler.js"; import userPurchaseHandler from "./handlers/userHandlers/userPurchaseHandler.js"; import userLocationHandler from "./handlers/userHandlers/userLocationHandler.js"; diff --git a/src/migrations/001_initial_schema.js b/src/migrations/001_initial_schema.js new file mode 100644 index 0000000..a0f8c01 --- /dev/null +++ b/src/migrations/001_initial_schema.js @@ -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'); +} diff --git a/src/migrations/002_add_columns.js b/src/migrations/002_add_columns.js new file mode 100644 index 0000000..5dd41d8 --- /dev/null +++ b/src/migrations/002_add_columns.js @@ -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'); +} diff --git a/src/migrations/003_add_indexes.js b/src/migrations/003_add_indexes.js new file mode 100644 index 0000000..79d9728 --- /dev/null +++ b/src/migrations/003_add_indexes.js @@ -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'); +} diff --git a/src/migrations/runner.js b/src/migrations/runner.js new file mode 100644 index 0000000..892cac6 --- /dev/null +++ b/src/migrations/runner.js @@ -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}`); +}