packages feed

ihp-ide-1.4.0: IHP/IDE/SchemaDesigner/Controller/Migrations.hs

module IHP.IDE.SchemaDesigner.Controller.Migrations where

import IHP.ControllerPrelude
import IHP.IDE.ToolServer.Types

import IHP.IDE.SchemaDesigner.View.Migrations.Index
import IHP.IDE.SchemaDesigner.View.Migrations.New
import IHP.IDE.SchemaDesigner.View.Migrations.Edit

import IHP.IDE.SchemaDesigner.View.Layout (schemaDesignerLayout)

import qualified Data.Text.IO as Text

import qualified IHP.SchemaMigration as SchemaMigration
import qualified IHP.IDE.CodeGen.MigrationGenerator as MigrationGenerator
import IHP.IDE.CodeGen.Controller
import IHP.IDE.ToolServer.Helper.Controller (openEditor, clearDatabaseNeedsMigration)
import IHP.Log.Types
import qualified Control.Exception.Safe as Exception
import qualified System.Directory as Directory
import qualified Database.PostgreSQL.Simple as PG

instance Controller MigrationsController where
    beforeAction = setLayout schemaDesignerLayout

    action MigrationsAction = do
        migrations <- findRecentMigrations
        migratedRevisions <- findMigratedRevisions

        migrationsWithSql <- forM migrations $ \migration -> do
                sql <- readSqlStatements migration
                pure (migration, sql)

        lastError <- getSessionAndClear "last_migraton_error"
        render IndexView { .. }


    action NewMigrationAction = do
        let description = paramOrDefault "" "description"
        (_, plan) <- MigrationGenerator.buildPlan description Nothing
        let runMigration = paramOrDefault True "runMigration"
        render NewView { .. }

    action CreateMigrationAction = do
        let description = paramOrDefault "" "description"
        let sqlStatements = paramOrNothing "sqlStatements"
        (revision, plan) <- MigrationGenerator.buildPlan description sqlStatements
        let path = MigrationGenerator.migrationPathFromPlan plan

        executePlan plan

        let createOnly = paramOrDefault False "createOnly"
        if createOnly
            then do
                setSuccessMessage ("Migration generated: " <> path)
                openEditor path 0 0
            else do
                result <- Exception.try (migrateAppDB revision)
                case result of
                    Left (exception :: SomeException) -> do
                        let errorMessage = case fromException exception of
                                Just (exception :: EnhancedSqlError) -> cs exception.sqlError.sqlErrorMsg
                                Nothing -> tshow exception

                        setErrorMessage errorMessage
                        redirectTo MigrationsAction
                    Right _ -> do
                        clearDatabaseNeedsMigration
                        redirectTo MigrationsAction

        redirectTo MigrationsAction

    action EditMigrationAction { migrationId } = do
        migration <- findMigrationByRevision migrationId
        sqlStatements <- readSqlStatements migration

        render EditView { .. }

    action UpdateMigrationAction { migrationId } = do
        migration <- findMigrationByRevision migrationId
        let sqlStatements = param "sqlStatements"
        migrationFilePath <- SchemaMigration.migrationPath migration
        Text.writeFile (cs migrationFilePath) sqlStatements

        redirectTo MigrationsAction

    action DeleteMigrationAction { migrationId } = do
        migration <- findMigrationByRevision migrationId
        path <- cs <$> SchemaMigration.migrationPath migration

        Directory.removeFile path

        redirectTo MigrationsAction

    action RunMigrationAction { migrationId } = do
        migration <- findMigrationByRevision migrationId

        result <- Exception.try (migrateAppDB migrationId)
        case result of
            Left (exception :: SomeException) -> do
                let errorMessage = case fromException exception of
                        Just (exception :: EnhancedSqlError) -> cs exception.sqlError.sqlErrorMsg
                        Nothing -> tshow exception

                setErrorMessage errorMessage
                redirectTo MigrationsAction
            Right _ -> do
                clearDatabaseNeedsMigration
                redirectTo MigrationsAction

readSqlStatements :: SchemaMigration.Migration -> IO Text
readSqlStatements migration = do
    migrationFilePath <- (SchemaMigration.migrationPath migration)
    Text.readFile (cs migrationFilePath)

findRecentMigrations :: IO [SchemaMigration.Migration]
findRecentMigrations = take 20 . reverse <$> SchemaMigration.findAllMigrations

findMigrationByRevision :: Int -> IO SchemaMigration.Migration
findMigrationByRevision migrationRevision = do
    migrations <- findRecentMigrations
    let (Just migration) = migrations |> find (\SchemaMigration.Migration { revision } -> revision == migrationRevision)
    pure migration

migrateAppDB :: Int -> IO ()
migrateAppDB revision = withAppModelContext do
    let minimumRevision = Just (revision - 1)
    SchemaMigration.migrate SchemaMigration.MigrateOptions { minimumRevision }

findMigratedRevisions :: IO [Int]
findMigratedRevisions = emptyListIfTablesDoesntExists (withAppModelContext SchemaMigration.findMigratedRevisions)
    where
        -- The schema_migrations table might not have been created yet
        -- In that case there cannot be any migrations that have been run yet
        emptyListIfTablesDoesntExists operation = do
            result <- Exception.try operation
            case result of
                Left (EnhancedSqlError { sqlError }) | sqlError.sqlErrorMsg == "relation \"schema_migrations\" does not exist" -> pure []
                Right result -> pure result

withAppModelContext :: ((?modelContext :: ModelContext) => IO result) -> IO result
withAppModelContext inner =
        Exception.bracket initModelContext cleanupModelContext callback
    where
        callback (frameworkConfig, logger, modelContext) = let ?modelContext = modelContext in inner
        initModelContext = do
            frameworkConfig <- buildFrameworkConfig (pure ())
            logger <- defaultLogger

            modelContext <- createModelContext
                (frameworkConfig.dbPoolIdleTime)
                (frameworkConfig.dbPoolMaxConnections)
                (frameworkConfig.databaseUrl)
                logger

            pure (frameworkConfig, logger, modelContext)

        cleanupModelContext (frameworkConfig, logger, modelContext) = do
            logger |> cleanup