UNPKG

@iarayan/ch-orm

Version:

A Developer-First ClickHouse ORM with Powerful CLI Tools

354 lines 14.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.MigrationRunnerCommand = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const MigrationRunner_1 = require("../../schema/MigrationRunner"); const MigrationRecord_1 = require("../../schema/models/MigrationRecord"); /** * Command for running migrations via CLI */ class MigrationRunnerCommand { constructor(connection, migrationsDir = "./migrations") { // Map of filename to migration instance this.migrationInstances = new Map(); this.migrationsDir = migrationsDir; this.runner = new MigrationRunner_1.MigrationRunner(connection); // Set connection for the MigrationRecord model MigrationRecord_1.MigrationRecord.setConnection(connection); } /** * Load a migration file by filename * @param filename Migration filename * @returns Migration instance */ async loadMigration(filename) { if (this.migrationInstances.has(filename)) { return this.migrationInstances.get(filename); } const filePath = path.resolve(this.migrationsDir, filename); if (!fs.existsSync(filePath)) { throw new Error(`Migration file not found: ${filename}`); } // Try ESM import first try { const migration = await Promise.resolve(`${filePath}`).then(s => __importStar(require(s))); const MigrationClass = migration.default; if (MigrationClass) { const instance = new MigrationClass(this.runner.getConnection()); // Override the getName method to return the filename const originalGetName = instance.getName.bind(instance); instance.getName = () => filename; // Store for reuse this.migrationInstances.set(filename, instance); return instance; } } catch (error) { // If ESM import fails, try CommonJS require with ts-node try { // Register ts-node if not already registered if (!require.extensions[".ts"]) { require("ts-node").register({ transpileOnly: true, compilerOptions: { module: "CommonJS", esModuleInterop: true, }, }); } const migration = require(filePath); // For CommonJS, find the class in the exports let MigrationClass = migration.default; // If no default export, look for named exports if (!MigrationClass) { for (const key in migration) { if (typeof migration[key] === "function" && migration[key].prototype && typeof migration[key].prototype.up === "function" && typeof migration[key].prototype.down === "function") { MigrationClass = migration[key]; break; } } } if (MigrationClass) { const instance = new MigrationClass(this.runner.getConnection()); // Override the getName method to return the filename const originalGetName = instance.getName.bind(instance); instance.getName = () => filename; // Store for reuse this.migrationInstances.set(filename, instance); return instance; } } catch (requireError) { throw new Error(`Failed to load migration file ${filename}: ${(requireError === null || requireError === void 0 ? void 0 : requireError.message) || "Unknown error"}`); } } throw new Error(`No migration class found in ${filename}`); } /** * Get all completed migrations */ async getCompletedMigrations() { // Use the MigrationRecord model to get all completed migrations const records = await MigrationRecord_1.MigrationRecord.query().get(); return records.map((record) => record.name); } /** * Get migrations for rollback */ async getMigrationsForRollback(steps = 1) { // Use the MigrationRecord model to get migrations for rollback const records = await MigrationRecord_1.MigrationRecord.query() .orderBy("created_at", "DESC") .limit(steps) .get(); return records.map((record) => record.name); } /** * Run a specific migration */ async runMigration(filename) { // Load the migration const instance = await this.loadMigration(filename); // Add it to the runner this.runner.add(instance); // Run it await this.runner.run(); } /** * Rollback a specific migration */ async rollbackMigration(filename) { // Load the migration const instance = await this.loadMigration(filename); // Add it to the runner this.runner.add(instance); // Mark it as applied (so it can be rolled back) instance.setApplied(true); // Roll it back await instance.revert(); // Delete the migration record from the database await MigrationRecord_1.MigrationRecord.query().where("name", filename).delete(); } /** * Get migration status */ async getMigrationStatus() { const completedMigrationNames = await this.getCompletedMigrations(); const files = fs.readdirSync(this.migrationsDir); return files.map((file) => { return { migration: file, status: completedMigrationNames.includes(file) ? "Completed" : "Pending", }; }); } /** * Run all pending migrations */ async run() { const status = await this.getMigrationStatus(); const pending = status.filter((s) => s.status === "Pending"); for (const { migration } of pending) { await this.runMigration(migration); } } /** * Rollback migrations */ async rollback() { // Get the last batch number const lastBatchResult = await MigrationRecord_1.MigrationRecord.query() .select("batch") .orderBy("batch", "DESC") .first(); if (!lastBatchResult) { return; // No migrations to roll back } const lastBatch = lastBatchResult.batch; // Get all migrations from the last batch const migrationsToRollback = await MigrationRecord_1.MigrationRecord.query() .where("batch", lastBatch) .orderBy("created_at", "DESC") // Roll back in reverse order of creation .get(); if (migrationsToRollback.length === 0) { return; // No migrations to roll back } // Load and roll back each migration in the batch for (const migrationRecord of migrationsToRollback) { try { await this.rollbackMigration(migrationRecord.name); } catch (error) { throw new Error(`Failed to roll back migration ${migrationRecord.name}: ${error.message}`); } } } /** * Reset the database by rolling back all migrations */ async reset() { const completedMigrationFilenames = await this.getCompletedMigrations(); if (completedMigrationFilenames.length === 0) { return; // No migrations to reset } // For each completed migration, load and add it to the runner for (const filename of completedMigrationFilenames) { try { const instance = await this.loadMigration(filename); this.runner.add(instance); instance.setApplied(true); } catch (error) { throw new Error(`Failed to load migration ${filename}: ${error.message}`); } } // Execute reset to roll back all migrations await this.runner.reset(); } /** * Fresh install - drops all database objects and runs migrations from scratch */ async fresh() { var _a, _b; // Get the connection to execute raw queries const connection = this.runner.getConnection(); try { // 1. Get list of all tables in the current database const result = await connection.query(` SELECT name, database FROM system.tables WHERE database = currentDatabase() AND name != 'migrations' -- Keep the migrations table for now AND NOT startsWith(name, '.inner') -- Skip internal system tables `); const tables = result.data.map((row) => ({ name: row.name, database: row.database, })); // 2. Get list of all materialized views const viewsResult = await connection.query(` SELECT name, database FROM system.tables WHERE database = currentDatabase() AND engine = 'MaterializedView' `); const views = viewsResult.data.map((row) => ({ name: row.name, database: row.database, })); // Count of inner tables (for reporting only) const innerTablesResult = await connection.query(` SELECT count() as count FROM system.tables WHERE database = currentDatabase() AND startsWith(name, '.inner') `); const innerTablesCount = ((_a = innerTablesResult.data[0]) === null || _a === void 0 ? void 0 : _a.count) || 0; // 3. Drop all materialized views first (they depend on tables) for (const view of views) { // Properly escape view names with backticks await connection.query(`DROP VIEW IF EXISTS \`${view.database}\`.\`${view.name}\``); } // 4. Drop all tables for (const table of tables) { if (table.name !== "migrations") { // Extra safety check // Properly escape table names with backticks await connection.query(`DROP TABLE IF EXISTS \`${table.database}\`.\`${table.name}\``); } } // 5. Check if migrations table exists and delete all records const migrationTableExistsResult = await connection.query(` SELECT count() as count FROM system.tables WHERE database = currentDatabase() AND name = 'migrations' `); const migrationsTableExists = ((_b = migrationTableExistsResult.data[0]) === null || _b === void 0 ? void 0 : _b.count) > 0; if (migrationsTableExists) { // Using the model to delete all migration records await MigrationRecord_1.MigrationRecord.query().where("1", "=", "1").deleteQuery(); // Double-check that the records are gone const countCheck = await MigrationRecord_1.MigrationRecord.query().count(); if (countCheck > 0) { // If model-based deletion didn't work, try direct SQL await connection.query(`DELETE FROM \`migrations\` WHERE 1=1`); } } else { // Create migrations table if it doesn't exist await connection.query(` CREATE TABLE IF NOT EXISTS \`migrations\` ( name String, batch UInt32, execution_time Float64, created_at DateTime ) ENGINE = MergeTree() ORDER BY (name) `); } // Verify migrations are clear before running new ones const migrationRecords = await MigrationRecord_1.MigrationRecord.query().get(); if (migrationRecords.length > 0) { throw new Error("Migration records could not be cleared"); } // 6. Run all migrations from scratch await this.run(); // Final verification await this.getMigrationStatus(); } catch (error) { throw error; } } /** * Refresh the database by rolling back all migrations and then running them again */ async refresh() { // First reset all migrations await this.reset(); // Then run all migrations again await this.run(); } } exports.MigrationRunnerCommand = MigrationRunnerCommand; //# sourceMappingURL=MigrationRunnerCommand.js.map