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:
@@ -1,19 +1,7 @@
|
|||||||
// database.js
|
|
||||||
|
|
||||||
import sqlite3 from 'sqlite3';
|
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 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) => {
|
const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_CREATE | sqlite3.OPEN_READWRITE, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Database connection error:', 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');
|
console.log('Connected to SQLite database');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable foreign keys
|
|
||||||
db.run('PRAGMA foreign_keys = ON');
|
db.run('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
// Promisify database operations
|
db.runAsync = (sql, params = []) =>
|
||||||
const runAsync = (sql, params = []) => {
|
new Promise((resolve, reject) => {
|
||||||
return new Promise((resolve, reject) => {
|
db.run(sql, params, function(err) { if (err) reject(err); else resolve(this); });
|
||||||
db.run(sql, params, function(err) {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(this);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const allAsync = (sql, params = []) => {
|
db.allAsync = (sql, params = []) =>
|
||||||
return new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
db.all(sql, params, (err, rows) => {
|
db.all(sql, params, (err, rows) => { if (err) reject(err); else resolve(rows); });
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(rows);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const getAsync = (sql, params = []) => {
|
db.getAsync = (sql, params = []) =>
|
||||||
return new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
db.get(sql, params, (err, row) => {
|
db.get(sql, params, (err, row) => { if (err) reject(err); else resolve(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) => {
|
db.on('error', (err) => {
|
||||||
console.error('Database error:', err);
|
console.error('Database error:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle process termination
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
db.close((err) => {
|
db.close((err) => {
|
||||||
if (err) {
|
if (err) console.error('Error closing database:', err);
|
||||||
console.error('Error closing database:', err);
|
else console.log('Database connection closed');
|
||||||
} else {
|
|
||||||
console.log('Database connection closed');
|
|
||||||
}
|
|
||||||
process.exit(err ? 1 : 0);
|
process.exit(err ? 1 : 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import { runMigrations, cleanUpInvalidForeignKeys } from './migrations/runner.js';
|
||||||
import adminUserHandler from './handlers/adminHandlers/adminUserHandler.js';
|
import adminUserHandler from './handlers/adminHandlers/adminUserHandler.js';
|
||||||
import ErrorHandler from './utils/errorHandler.js';
|
import ErrorHandler from './utils/errorHandler.js';
|
||||||
import bot from "./context/bot.js";
|
import bot from "./context/bot.js";
|
||||||
|
|
||||||
|
await runMigrations();
|
||||||
|
await cleanUpInvalidForeignKeys();
|
||||||
import userHandler from "./handlers/userHandlers/userHandler.js";
|
import userHandler from "./handlers/userHandlers/userHandler.js";
|
||||||
import userPurchaseHandler from "./handlers/userHandlers/userPurchaseHandler.js";
|
import userPurchaseHandler from "./handlers/userHandlers/userPurchaseHandler.js";
|
||||||
import userLocationHandler from "./handlers/userHandlers/userLocationHandler.js";
|
import userLocationHandler from "./handlers/userHandlers/userLocationHandler.js";
|
||||||
|
|||||||
92
src/migrations/001_initial_schema.js
Normal file
92
src/migrations/001_initial_schema.js
Normal 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');
|
||||||
|
}
|
||||||
33
src/migrations/002_add_columns.js
Normal file
33
src/migrations/002_add_columns.js
Normal 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');
|
||||||
|
}
|
||||||
9
src/migrations/003_add_indexes.js
Normal file
9
src/migrations/003_add_indexes.js
Normal 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
56
src/migrations/runner.js
Normal 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}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user