"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MigrationExecutor = void 0;
const Table_1 = require("../schema-builder/table/Table");
const Migration_1 = require("./Migration");
const MssqlParameter_1 = require("../driver/sqlserver/MssqlParameter");
const error_1 = require("../error");
const InstanceChecker_1 = require("../util/InstanceChecker");
/**
 * Executes migrations: runs pending and reverts previously executed migrations.
 */
class MigrationExecutor {
    // -------------------------------------------------------------------------
    // Constructor
    // -------------------------------------------------------------------------
    constructor(dataSource, queryRunner) {
        this.dataSource = dataSource;
        this.queryRunner = queryRunner;
        // -------------------------------------------------------------------------
        // Public Properties
        // -------------------------------------------------------------------------
        /**
         * Indicates how migrations should be run in transactions.
         *   all: all migrations are run in a single transaction
         *   none: all migrations are run without a transaction
         *   each: each migration is run in a separate transaction
         */
        this.transaction = "all";
        const { schema } = this.dataSource.driver.options;
        const database = this.dataSource.driver.database;
        this.migrationsDatabase = database;
        this.migrationsSchema = schema;
        this.migrationsTableName =
            dataSource.options.migrationsTableName || "migrations";
        this.migrationsTable = this.dataSource.driver.buildTableName(this.migrationsTableName, schema, database);
    }
    // -------------------------------------------------------------------------
    // Public Methods
    // -------------------------------------------------------------------------
    /**
     * Tries to execute a single migration given.
     * @param migration
     */
    async executeMigration(migration) {
        return this.withQueryRunner(async (queryRunner) => {
            await this.createMigrationsTableIfNotExist(queryRunner);
            // create typeorm_metadata table if it's not created yet
            const schemaBuilder = this.dataSource.driver.createSchemaBuilder();
            if (InstanceChecker_1.InstanceChecker.isRdbmsSchemaBuilder(schemaBuilder)) {
                await schemaBuilder.createMetadataTableIfNecessary(queryRunner);
            }
            await queryRunner.beforeMigration();
            await migration.instance.up(queryRunner);
            await queryRunner.afterMigration();
            await this.insertExecutedMigration(queryRunner, migration);
            return migration;
        });
    }
    /**
     * @returns An array of all executed migrations
     */
    async getExecutedMigrations() {
        return this.withQueryRunner(async (queryRunner) => {
            // There is no need to check if migrations table exists for MongoDB,
            // as it's handled in loadExecutedMigrations
            if (this.dataSource.driver.options.type !== "mongodb") {
                const exist = await queryRunner.hasTable(this.migrationsTable);
                if (!exist)
                    return [];
            }
            return await this.loadExecutedMigrations(queryRunner);
        });
    }
    /**
     * Returns an array of all pending migrations.
     */
    async getPendingMigrations() {
        const allMigrations = this.getMigrations();
        const executedMigrations = await this.getExecutedMigrations();
        if (executedMigrations.length === 0)
            return allMigrations;
        return allMigrations.filter((migration) => !executedMigrations.find((executedMigration) => executedMigration.name === migration.name));
    }
    /**
     * Inserts an executed migration.
     * @param migration
     */
    insertMigration(migration) {
        return this.withQueryRunner((q) => this.insertExecutedMigration(q, migration));
    }
    /**
     * Deletes an executed migration.
     * @param migration
     */
    deleteMigration(migration) {
        return this.withQueryRunner((q) => this.deleteExecutedMigration(q, migration));
    }
    /**
     * Lists all migrations and whether they have been executed or not
     * returns true if there are unapplied migrations
     */
    async showMigrations() {
        let hasUnappliedMigrations = false;
        const queryRunner = this.queryRunner || this.dataSource.createQueryRunner();
        // create migrations table if its not created yet
        await this.createMigrationsTableIfNotExist(queryRunner);
        // get all migrations that are executed and saved in the database
        const executedMigrations = await this.loadExecutedMigrations(queryRunner);
        // get all user's migrations in the source code
        const allMigrations = this.getMigrations();
        for (const migration of allMigrations) {
            const executedMigration = executedMigrations.find((executedMigration) => executedMigration.name === migration.name);
            if (executedMigration) {
                this.dataSource.logger.logSchemaBuild(`[X] ${executedMigration.id} ${migration.name}`);
            }
            else {
                hasUnappliedMigrations = true;
                this.dataSource.logger.logSchemaBuild(`[ ] ${migration.name}`);
            }
        }
        // if query runner was created by us then release it
        if (!this.queryRunner) {
            await queryRunner.release();
        }
        return hasUnappliedMigrations;
    }
    /**
     * Executes all pending migrations. Pending migrations are migrations that are not yet executed,
     * thus not saved in the database.
     */
    async executePendingMigrations() {
        const queryRunner = this.queryRunner || this.dataSource.createQueryRunner();
        // create migrations table if it's not created yet
        await this.createMigrationsTableIfNotExist(queryRunner);
        // create the typeorm_metadata table if it's not created yet
        const schemaBuilder = this.dataSource.driver.createSchemaBuilder();
        if (InstanceChecker_1.InstanceChecker.isRdbmsSchemaBuilder(schemaBuilder)) {
            await schemaBuilder.createMetadataTableIfNecessary(queryRunner);
        }
        // get all migrations that are executed and saved in the database
        const executedMigrations = await this.loadExecutedMigrations(queryRunner);
        // get the time when last migration was executed
        const lastTimeExecutedMigration = this.getLatestTimestampMigration(executedMigrations);
        // get all user's migrations in the source code
        const allMigrations = this.getMigrations();
        // variable to store all migrations we did successfully
        const successMigrations = [];
        // find all migrations that needs to be executed
        const pendingMigrations = allMigrations.filter((migration) => {
            // check if we already have executed migration
            const executedMigration = executedMigrations.find((executedMigration) => executedMigration.name === migration.name);
            if (executedMigration)
                return false;
            // migration is new and not executed. now check if its timestamp is correct
            // if (lastTimeExecutedMigration && migration.timestamp < lastTimeExecutedMigration.timestamp)
            //     throw new TypeORMError(`New migration found: ${migration.name}, however this migration's timestamp is not valid. Migration's timestamp should not be older then migrations already executed in the database.`);
            // every check is passed means that migration was not run yet and we need to run it
            return true;
        });
        // if no migrations are pending then nothing to do here
        if (!pendingMigrations.length) {
            this.dataSource.logger.logSchemaBuild(`No migrations are pending`);
            // if query runner was created by us then release it
            if (!this.queryRunner)
                await queryRunner.release();
            return [];
        }
        // log information about migration execution
        this.dataSource.logger.logSchemaBuild(`${executedMigrations.length} migrations are already loaded in the database.`);
        this.dataSource.logger.logSchemaBuild(`${allMigrations.length} migrations were found in the source code.`);
        if (lastTimeExecutedMigration)
            this.dataSource.logger.logSchemaBuild(`${lastTimeExecutedMigration.name} is the last executed migration. It was executed on ${new Date(lastTimeExecutedMigration.timestamp).toString()}.`);
        this.dataSource.logger.logSchemaBuild(`${pendingMigrations.length} migrations are new migrations must be executed.`);
        if (this.transaction === "all") {
            // If we desire to run all migrations in a single transaction
            // but there is a migration that explicitly overrides the transaction mode
            // then we have to fail since we cannot properly resolve that intent
            // In theory we could support overrides that are set to `true`,
            // however to keep the interface more rigid, we fail those too
            const migrationsOverridingTransactionMode = pendingMigrations.filter((migration) => !(migration.instance?.transaction === undefined));
            if (migrationsOverridingTransactionMode.length > 0) {
                const error = new error_1.ForbiddenTransactionModeOverrideError(migrationsOverridingTransactionMode);
                this.dataSource.logger.logMigration(`Migrations failed, error: ${error.message}`);
                throw error;
            }
        }
        // Set the per-migration defaults for the transaction mode
        // so that we have one centralized place that controls this behavior
        // When transaction mode is `each` the default is to run in a transaction
        // When transaction mode is `none` the default is to not run in a transaction
        // When transaction mode is `all` the default is to not run in a transaction
        // since all the migrations are already running in one single transaction
        const txModeDefault = {
            each: true,
            none: false,
            all: false,
        }[this.transaction];
        for (const migration of pendingMigrations) {
            if (migration.instance) {
                const instanceTx = migration.instance.transaction;
                if (instanceTx === undefined) {
                    migration.transaction = txModeDefault;
                }
                else {
                    migration.transaction = instanceTx;
                }
            }
        }
        // start transaction if its not started yet
        let transactionStartedByUs = false;
        if (this.transaction === "all" && !queryRunner.isTransactionActive) {
            await queryRunner.beforeMigration();
            await queryRunner.startTransaction();
            transactionStartedByUs = true;
        }
        // run all pending migrations in a sequence
        try {
            for (const migration of pendingMigrations) {
                if (this.fake) {
                    // directly insert migration record into the database if it is fake
                    await this.insertExecutedMigration(queryRunner, migration);
                    // nothing else needs to be done, continue to next migration
                    continue;
                }
                if (migration.transaction && !queryRunner.isTransactionActive) {
                    await queryRunner.beforeMigration();
                    await queryRunner.startTransaction();
                    transactionStartedByUs = true;
                }
                await migration
                    .instance.up(queryRunner)
                    .catch((error) => {
                    // informative log about migration failure
                    this.dataSource.logger.logMigration(`Migration "${migration.name}" failed, error: ${error?.message}`);
                    throw error;
                })
                    .then(async () => {
                    // now when migration is executed we need to insert record about it into the database
                    await this.insertExecutedMigration(queryRunner, migration);
                    // commit transaction if we started it
                    if (migration.transaction && transactionStartedByUs) {
                        await queryRunner.commitTransaction();
                        await queryRunner.afterMigration();
                    }
                })
                    .then(() => {
                    // informative log about migration success
                    successMigrations.push(migration);
                    this.dataSource.logger.logSchemaBuild(`Migration ${migration.name} has been ${this.fake ? "(fake) " : ""}executed successfully.`);
                });
            }
            // commit transaction if we started it
            if (this.transaction === "all" && transactionStartedByUs) {
                await queryRunner.commitTransaction();
                await queryRunner.afterMigration();
            }
        }
        catch (err) {
            // rollback transaction if we started it
            if (transactionStartedByUs) {
                try {
                    // we throw original error even if rollback thrown an error
                    await queryRunner.rollbackTransaction();
                }
                catch (rollbackError) { }
            }
            throw err;
        }
        finally {
            // if query runner was created by us then release it
            if (!this.queryRunner)
                await queryRunner.release();
        }
        return successMigrations;
    }
    /**
     * Reverts last migration that were run.
     */
    async undoLastMigration() {
        const queryRunner = this.queryRunner || this.dataSource.createQueryRunner();
        // create migrations table if it's not created yet
        await this.createMigrationsTableIfNotExist(queryRunner);
        // create typeorm_metadata table if it's not created yet
        const schemaBuilder = this.dataSource.driver.createSchemaBuilder();
        if (InstanceChecker_1.InstanceChecker.isRdbmsSchemaBuilder(schemaBuilder)) {
            await schemaBuilder.createMetadataTableIfNecessary(queryRunner);
        }
        // get all migrations that are executed and saved in the database
        const executedMigrations = await this.loadExecutedMigrations(queryRunner);
        // get the time when last migration was executed
        const lastTimeExecutedMigration = this.getLatestExecutedMigration(executedMigrations);
        // if no migrations found in the database then nothing to revert
        if (!lastTimeExecutedMigration) {
            this.dataSource.logger.logSchemaBuild(`No migrations were found in the database. Nothing to revert!`);
            // if query runner was created by us then release it
            if (!this.queryRunner)
                await queryRunner.release();
            return;
        }
        // get all user's migrations in the source code
        const allMigrations = this.getMigrations();
        // find the instance of the migration we need to remove
        const migrationToRevert = allMigrations.find((migration) => migration.name === lastTimeExecutedMigration.name);
        // if no migrations found in the database then nothing to revert
        if (!migrationToRevert)
            throw new error_1.TypeORMError(`No migration ${lastTimeExecutedMigration.name} was found in the source code. Make sure you have this migration in your codebase and its included in the connection options.`);
        // log information about migration execution
        this.dataSource.logger.logSchemaBuild(`${executedMigrations.length} migrations are already loaded in the database.`);
        this.dataSource.logger.logSchemaBuild(`${lastTimeExecutedMigration.name} is the last executed migration. It was executed on ${new Date(lastTimeExecutedMigration.timestamp).toString()}.`);
        this.dataSource.logger.logSchemaBuild(`Now reverting it...`);
        // start transaction if its not started yet
        let transactionStartedByUs = false;
        if (this.transaction !== "none" && !queryRunner.isTransactionActive) {
            await queryRunner.startTransaction();
            transactionStartedByUs = true;
        }
        try {
            if (!this.fake) {
                await queryRunner.beforeMigration();
                await migrationToRevert.instance.down(queryRunner);
                await queryRunner.afterMigration();
            }
            await this.deleteExecutedMigration(queryRunner, migrationToRevert);
            this.dataSource.logger.logSchemaBuild(`Migration ${migrationToRevert.name} has been ${this.fake ? "(fake) " : ""}reverted successfully.`);
            // commit transaction if we started it
            if (transactionStartedByUs)
                await queryRunner.commitTransaction();
        }
        catch (err) {
            // rollback transaction if we started it
            if (transactionStartedByUs) {
                try {
                    // we throw original error even if rollback thrown an error
                    await queryRunner.rollbackTransaction();
                }
                catch (rollbackError) { }
            }
            throw err;
        }
        finally {
            // if query runner was created by us then release it
            if (!this.queryRunner)
                await queryRunner.release();
        }
    }
    // -------------------------------------------------------------------------
    // Protected Methods
    // -------------------------------------------------------------------------
    /**
     * Creates table "migrations" that will store information about executed migrations.
     * @param queryRunner
     */
    async createMigrationsTableIfNotExist(queryRunner) {
        // If driver is mongo no need to create
        if (this.dataSource.driver.options.type === "mongodb") {
            return;
        }
        const tableExist = await queryRunner.hasTable(this.migrationsTable); // todo: table name should be configurable
        if (!tableExist) {
            await queryRunner.createTable(new Table_1.Table({
                database: this.migrationsDatabase,
                schema: this.migrationsSchema,
                name: this.migrationsTable,
                columns: [
                    {
                        name: "id",
                        type: this.dataSource.driver.normalizeType({
                            type: this.dataSource.driver.mappedDataTypes
                                .migrationId,
                        }),
                        isGenerated: true,
                        generationStrategy: "increment",
                        isPrimary: true,
                        isNullable: false,
                    },
                    {
                        name: "timestamp",
                        type: this.dataSource.driver.normalizeType({
                            type: this.dataSource.driver.mappedDataTypes
                                .migrationTimestamp,
                        }),
                        isPrimary: false,
                        isNullable: false,
                    },
                    {
                        name: "name",
                        type: this.dataSource.driver.normalizeType({
                            type: this.dataSource.driver.mappedDataTypes
                                .migrationName,
                        }),
                        isNullable: false,
                    },
                ],
            }));
        }
    }
    /**
     * Loads all migrations that were executed and saved into the database (sorts by id).
     * @param queryRunner
     */
    async loadExecutedMigrations(queryRunner) {
        if (this.dataSource.driver.options.type === "mongodb") {
            const mongoRunner = queryRunner;
            return mongoRunner
                .cursor(this.migrationsTableName, {})
                .sort({ _id: -1 })
                .toArray();
        }
        else {
            const migrationsRaw = await this.dataSource.manager
                .createQueryBuilder(queryRunner)
                .select()
                .orderBy(this.dataSource.driver.escape("id"), "DESC")
                .from(this.migrationsTable, this.migrationsTableName)
                .getRawMany();
            return migrationsRaw.map((migrationRaw) => {
                return new Migration_1.Migration(parseInt(migrationRaw["id"]), parseInt(migrationRaw["timestamp"]), migrationRaw["name"]);
            });
        }
    }
    /**
     * Gets all migrations that setup for this connection.
     */
    getMigrations() {
        const migrations = this.dataSource.migrations.map((migration) => {
            const migrationClassName = migration.name || migration.constructor.name;
            const migrationTimestamp = parseInt(migrationClassName.substr(-13), 10);
            if (!migrationTimestamp || isNaN(migrationTimestamp)) {
                throw new error_1.TypeORMError(`${migrationClassName} migration name is wrong. Migration class name should have a JavaScript timestamp appended.`);
            }
            return new Migration_1.Migration(undefined, migrationTimestamp, migrationClassName, migration);
        });
        this.checkForDuplicateMigrations(migrations);
        // sort them by timestamp
        return migrations.sort((a, b) => a.timestamp - b.timestamp);
    }
    checkForDuplicateMigrations(migrations) {
        const migrationNames = migrations.map((migration) => migration.name);
        const duplicates = Array.from(new Set(migrationNames.filter((migrationName, index) => migrationNames.indexOf(migrationName) < index)));
        if (duplicates.length > 0) {
            throw Error(`Duplicate migrations: ${duplicates.join(", ")}`);
        }
    }
    /**
     * Finds the latest migration (sorts by timestamp) in the given array of migrations.
     * @param migrations
     */
    getLatestTimestampMigration(migrations) {
        const sortedMigrations = migrations
            .map((migration) => migration)
            .sort((a, b) => (a.timestamp - b.timestamp) * -1);
        return sortedMigrations.length > 0 ? sortedMigrations[0] : undefined;
    }
    /**
     * Finds the latest migration in the given array of migrations.
     * PRE: Migration array must be sorted by descending id.
     * @param sortedMigrations
     */
    getLatestExecutedMigration(sortedMigrations) {
        return sortedMigrations.length > 0 ? sortedMigrations[0] : undefined;
    }
    /**
     * Inserts new executed migration's data into migrations table.
     * @param queryRunner
     * @param migration
     */
    async insertExecutedMigration(queryRunner, migration) {
        const values = {};
        if (this.dataSource.driver.options.type === "mssql") {
            values["timestamp"] = new MssqlParameter_1.MssqlParameter(migration.timestamp, this.dataSource.driver.normalizeType({
                type: this.dataSource.driver.mappedDataTypes
                    .migrationTimestamp,
            }));
            values["name"] = new MssqlParameter_1.MssqlParameter(migration.name, this.dataSource.driver.normalizeType({
                type: this.dataSource.driver.mappedDataTypes.migrationName,
            }));
        }
        else {
            values["timestamp"] = migration.timestamp;
            values["name"] = migration.name;
        }
        if (this.dataSource.driver.options.type === "mongodb") {
            const mongoRunner = queryRunner;
            await mongoRunner.databaseConnection
                .db(this.dataSource.driver.database)
                .collection(this.migrationsTableName)
                .insertOne(values);
        }
        else {
            const qb = queryRunner.manager.createQueryBuilder();
            await qb
                .insert()
                .into(this.migrationsTable)
                .values(values)
                .execute();
        }
    }
    /**
     * Delete previously executed migration's data from the migrations table.
     * @param queryRunner
     * @param migration
     */
    async deleteExecutedMigration(queryRunner, migration) {
        const conditions = {};
        if (this.dataSource.driver.options.type === "mssql") {
            conditions["timestamp"] = new MssqlParameter_1.MssqlParameter(migration.timestamp, this.dataSource.driver.normalizeType({
                type: this.dataSource.driver.mappedDataTypes
                    .migrationTimestamp,
            }));
            conditions["name"] = new MssqlParameter_1.MssqlParameter(migration.name, this.dataSource.driver.normalizeType({
                type: this.dataSource.driver.mappedDataTypes.migrationName,
            }));
        }
        else {
            conditions["timestamp"] = migration.timestamp;
            conditions["name"] = migration.name;
        }
        if (this.dataSource.driver.options.type === "mongodb") {
            const mongoRunner = queryRunner;
            await mongoRunner.databaseConnection
                .db(this.dataSource.driver.database)
                .collection(this.migrationsTableName)
                .deleteOne(conditions);
        }
        else {
            const qb = queryRunner.manager.createQueryBuilder();
            await qb
                .delete()
                .from(this.migrationsTable)
                .where(`${qb.escape("timestamp")} = :timestamp`)
                .andWhere(`${qb.escape("name")} = :name`)
                .setParameters(conditions)
                .execute();
        }
    }
    async withQueryRunner(callback) {
        const queryRunner = this.queryRunner || this.dataSource.createQueryRunner();
        try {
            return await callback(queryRunner);
        }
        finally {
            if (!this.queryRunner) {
                await queryRunner.release();
            }
        }
    }
}
exports.MigrationExecutor = MigrationExecutor;
//# sourceMappingURL=MigrationExecutor.js.map