Skip to content

Commit

Permalink
Merge branch 'master' into simplify-migrate-service
Browse files Browse the repository at this point in the history
  • Loading branch information
mpscholten authored Nov 23, 2024
2 parents 5684d1a + 611b881 commit 1a0c987
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 30 deletions.
14 changes: 14 additions & 0 deletions Guide/database-migrations.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ migrate

A good value for `MINIMUM_REVISION` is typically the unix timestamp of the time when the database was initially created.


### IHP MIGRATIONS DIR

In production when running the migrations binary it is sometimes convenient to have all your Migrations in a non-standard place:
e.g. if you need to push migrations onto production server without rebuilding the application. There is an Environment variable
`IHP_MIGRATION_DIR` to accomplish this.

```
IHP_MIGRATION_DIR=path/to/my/migration/dir
```

This can be set in the environment attribute set of your IHP app flake.


## Common Issues

### ALTER TYPE ... ADD cannot run inside a transaction block
Expand Down
19 changes: 15 additions & 4 deletions IHP/SchemaMigration.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import qualified Data.Text.IO as Text
import IHP.ModelSupport hiding (withTransaction)
import qualified Data.Char as Char
import IHP.Log.Types
import IHP.EnvVar

data Migration = Migration
{ revision :: Int
Expand All @@ -37,7 +38,9 @@ migrate options = do
-- All queries are executed inside a database transaction to make sure that it can be restored when something goes wrong.
runMigration :: (?modelContext :: ModelContext) => Migration -> IO ()
runMigration migration@Migration { revision, migrationFile } = do
migrationSql <- Text.readFile (cs $ migrationPath migration)
-- | User can specify migrations directory as environment variable (defaults to /Application/Migrations/...)
migrationFilePath <- migrationPath migration
migrationSql <- Text.readFile (cs migrationFilePath)

let fullSql = [trimming|
BEGIN;
Expand Down Expand Up @@ -96,7 +99,8 @@ findMigratedRevisions = map (\[revision] -> revision) <$> sqlQuery "SELECT revis
-- The result is sorted so that the oldest revision is first.
findAllMigrations :: IO [Migration]
findAllMigrations = do
directoryFiles <- Directory.listDirectory "Application/Migration"
migrationDir <- detectMigrationDir
directoryFiles <- Directory.listDirectory (cs migrationDir)
directoryFiles
|> map cs
|> filter (\path -> ".sql" `isSuffixOf` path)
Expand All @@ -123,5 +127,12 @@ pathToMigration fileName = case revision of
|> fmap textToInt
|> join

migrationPath :: Migration -> Text
migrationPath Migration { migrationFile } = "Application/Migration/" <> migrationFile
migrationPath :: Migration -> IO Text
migrationPath Migration { migrationFile } = do
migrationDir <- detectMigrationDir
pure (migrationDir <> migrationFile)

detectMigrationDir :: IO Text
detectMigrationDir =
envOrDefault "IHP_MIGRATION_DIR" "Application/Migration/"

36 changes: 21 additions & 15 deletions NixSupport/nixosModules/options.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,63 +13,70 @@ with lib;
type = types.str;
default = "https://${config.services.ihp.domain}";
};

migrations = mkOption {
type = types.path;
};

schema = mkOption {
type = types.path;
};

fixtures = mkOption {
type = types.path;
};

httpsEnabled = mkOption {
type = types.bool;
default = true;
};

databaseName = mkOption {
type = types.str;
default = "app";
};

databaseUser = mkOption {
type = types.str;
default = "ihp";
};

databaseUrl = mkOption {
type = types.str;
};

# https://ihp.digitallyinduced.com/Guide/database-migrations.html#skipping-old-migrations
minimumRevision = mkOption {
type = types.int;
default = 0;
};


# https://ihp.digitallyinduced.com/Guide/database-migrations.html#ihp-migrations-dir
ihpMigrationDir = mkOption {
type = types.str;
default = "Application/Migration/";
};


ihpEnv = mkOption {
type = types.str;
default = "Production";
};

appPort = mkOption {
type = types.int;
default = 8000;
};

requestLoggerIPAddrSource = mkOption {
type = types.str;
default = "FromHeader";
};

sessionSecret = mkOption {
type = types.str;
};

additionalEnvVars = mkOption {
type = types.attrs;
default = {};
Expand All @@ -79,7 +86,7 @@ with lib;
type = types.package;
default = if config.services.ihp.optimized then self.packages."${pkgs.system}".optimized-prod-server else self.packages."${pkgs.system}".default;
};

optimized = mkOption {
type = types.bool;
default = false;
Expand All @@ -91,4 +98,3 @@ with lib;
};
};
}

2 changes: 1 addition & 1 deletion NixSupport/nixosModules/services/migrate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ in
IHP_MIGRATION_DIR = cfg.migrations;
};
};
}
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
</a>

<img src="https://api.scarf.sh/v2/packages/digitallyinduced/4b7241ee-9679-43fa-9b27-b5595b775c30/downloads-badge"/>

<a href="https://gurubase.io/g/ihp" target="_blank">
<img src="https://img.shields.io/badge/Gurubase-Ask%20IHP%20Guru-006BFF"/>
</a>
</p>

<p align="center">
Expand Down
14 changes: 8 additions & 6 deletions ihp-ide/IHP/IDE/SchemaDesigner/Controller/Migrations.hs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ instance Controller MigrationsController where
let errorMessage = case fromException exception of
Just (exception :: EnhancedSqlError) -> cs exception.sqlError.sqlErrorMsg
Nothing -> tshow exception

setErrorMessage errorMessage
redirectTo MigrationsAction
Right _ -> do
Expand All @@ -79,14 +79,14 @@ instance Controller MigrationsController where
action UpdateMigrationAction { migrationId } = do
migration <- findMigrationByRevision migrationId
let sqlStatements = param "sqlStatements"

Text.writeFile (cs $ SchemaMigration.migrationPath migration) sqlStatements
migrationFilePath <- SchemaMigration.migrationPath migration
Text.writeFile (cs migrationFilePath) sqlStatements

redirectTo MigrationsAction

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

Directory.removeFile path

Expand All @@ -101,15 +101,17 @@ instance Controller MigrationsController where
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 = Text.readFile (cs $ SchemaMigration.migrationPath migration)
readSqlStatements migration = do
migrationFilePath <- (SchemaMigration.migrationPath migration)
Text.readFile (cs migrationFilePath)

findRecentMigrations :: IO [SchemaMigration.Migration]
findRecentMigrations = take 20 . reverse <$> SchemaMigration.findAllMigrations
Expand Down
12 changes: 8 additions & 4 deletions lib/IHP/DataSync/ihp-datasync.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ class DataSubscription {
return;
}

// Set isClosed early as we need to prevent a second close() from triggering another DeleteDataSubscription message
// also we don't want to receive any further messages, and onMessage will not process if isClosed == true
this.isClosed = true;
this.onClose();

const dataSyncController = DataSyncController.getInstance();
const { subscriptionId } = await dataSyncController.sendMessage({ tag: 'DeleteDataSubscription', subscriptionId: this.subscriptionId });

Expand All @@ -360,10 +365,7 @@ class DataSubscription {
dataSyncController.removeEventListener('reconnect', this.onDataSyncReconnect);
dataSyncController.dataSubscriptions.splice(dataSyncController.dataSubscriptions.indexOf(this), 1);

this.isClosed = true;
this.isConnected = false;

this.onClose();
}

onDataSyncClosed() {
Expand Down Expand Up @@ -425,7 +427,9 @@ class DataSubscription {
return () => {
this.subscribers.splice(this.subscribers.indexOf(callback), 1);

this.closeIfNotUsed();
// We delay the close as react could be re-rendering a component
// we garbage collect this connecetion once it's clearly not used anymore
setTimeout(this.closeIfNotUsed.bind(this), 1000);
}
}

Expand Down

0 comments on commit 1a0c987

Please sign in to comment.