@nivinjoseph/n-data
Version:
Data access library for Postgres based on Knex
229 lines (169 loc) • 8.56 kB
text/typescript
import { Disposable } from "@nivinjoseph/n-util";
import { Container, ComponentInstaller, Registry, ServiceLocator } from "@nivinjoseph/n-ject";
import { given } from "@nivinjoseph/n-defensive";
import { Logger, ConsoleLogger } from "@nivinjoseph/n-log";
import { DbMigration } from "./db-migration.js";
import { DbVersionProvider } from "./db-version-provider.js";
import { SystemTablesProvider } from "./system/system-tables-provider.js";
import { DefaultSystemRepository } from "./system/default-system-repository.js";
import { DefaultDbVersionProvider } from "./default-db-version-provider.js";
import { MigrationDependencyKey } from "./migration-dependency-key.js";
export class DbMigrator implements Disposable
{
private readonly _container: Container;
private _logger!: Logger;
private readonly _migrationRegistrations: Array<MigrationRegistration>;
private _dbVersionProviderClass: Function | null = null;
private _systemTableName: string | null = null;
private _isDisposed: boolean;
private _isBootstrapped: boolean;
public get containerRegistry(): Registry { return this._container; }
public get serviceLocator(): ServiceLocator { return this._container; }
public constructor()
{
this._container = new Container();
this._migrationRegistrations = [];
this._isDisposed = false;
this._isBootstrapped = false;
}
public useLogger(logger: Logger): this
{
given(logger, "logger").ensureHasValue().ensureIsObject();
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._logger = logger;
return this;
}
public useInstaller(installer: ComponentInstaller): this
{
given(installer, "installer").ensureHasValue().ensureIsObject();
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._container.install(installer);
return this;
}
public useSystemTable(systemTableName: string): this
{
given(systemTableName, "systemTableName").ensureHasValue().ensureIsString()
.ensure(t => t.trim().toLowerCase() === t.trim(), "table name must be all lowercase");
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._systemTableName = systemTableName.trim().toLowerCase();
return this;
}
public registerDbVersionProvider(dbVersionProviderClass: Function): this
{
given(dbVersionProviderClass, "dbVersionProviderClass").ensureHasValue().ensureIsFunction();
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._dbVersionProviderClass = dbVersionProviderClass;
return this;
}
public registerMigrations(...migrationClasses: Array<Function>): this
{
given(migrationClasses, "migrationClasses").ensureHasValue().ensureIsArray().ensure(t => t.length > 0);
given(this, "this").ensure(t => !t._isBootstrapped, "invoking method after bootstrap");
this._migrationRegistrations.push(...migrationClasses.map(t => new MigrationRegistration(t)));
return this;
}
public bootstrap(): this
{
given(this, "this")
.ensure(t => !t._isBootstrapped, "invoking method after bootstrap")
.ensure(t => t._dbVersionProviderClass != null || t._systemTableName != null,
"one of either DbVersionProvider or SystemTableName must be provided")
.ensure(t => t._dbVersionProviderClass == null || t._systemTableName == null,
"cannot provide both DbVersionProvider and SystemTableName")
.ensure(t => t._migrationRegistrations.length > 0, "no migrations registered")
.ensure(t => t._migrationRegistrations.distinct(u => u.name).length === t._migrationRegistrations.length, "Duplicate registration names detected.")
.ensure(t => t._migrationRegistrations.distinct(u => u.version).length === t._migrationRegistrations.length, "Duplicate registration versions detected.")
;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this._logger == null)
this._logger = new ConsoleLogger();
if (this._dbVersionProviderClass != null)
this._container.registerSingleton(MigrationDependencyKey.dbVersionProvider, this._dbVersionProviderClass);
if (this._systemTableName != null)
{
this._container
.registerInstance(MigrationDependencyKey.dbSystemTablesProvider, <SystemTablesProvider>{ systemTableName: this._systemTableName })
.registerSingleton(MigrationDependencyKey.dbSystemRepository, DefaultSystemRepository)
.registerSingleton(MigrationDependencyKey.dbVersionProvider, DefaultDbVersionProvider);
}
this._migrationRegistrations.forEach(t => this._container.registerScoped(t.name, t.migration));
this._container.bootstrap();
this._isBootstrapped = true;
return this;
}
public async runMigrations(): Promise<void>
{
given(this, "this").ensure(t => t._isBootstrapped, "invoking method before bootstrap");
const dbVersionProvider = this._container.resolve<DbVersionProvider>(MigrationDependencyKey.dbVersionProvider);
await this._executeMigrations(dbVersionProvider);
}
public dispose(): Promise<void>
{
if (this._isDisposed)
return Promise.resolve();
this._isDisposed = true;
return this._container.dispose();
}
private async _executeMigrations(dbVersionProvider: DbVersionProvider): Promise<void>
{
given(dbVersionProvider, "dbVersionProvider").ensureHasValue().ensureIsObject();
const currentVersion = await dbVersionProvider.getVersion();
const migrationRegistrations = this._migrationRegistrations
.filter(t => t.version > currentVersion)
.orderBy(t => t.version);
await this._logger.logInfo("Commencing migrations.");
await this._logger.logInfo(`Current Db version is '${currentVersion}'.`);
if (migrationRegistrations.length === 0)
{
await this._logger.logWarning("No migrations to execute.");
}
else
{
await this._logger.logInfo(`${migrationRegistrations.length} migrations to execute starting with version '${migrationRegistrations[0].version}'.`);
for (const registration of migrationRegistrations)
{
await this._logger.logInfo(`Commencing migration ${registration.name}`);
const scope = this._container.createScope();
try
{
const migration = scope.resolve<DbMigration>(registration.name);
await migration.execute();
await dbVersionProvider.setVersion(registration.version);
await this._logger.logInfo(`Completed migration ${registration.name}`);
}
catch (error)
{
await this._logger.logWarning(`Failed migration ${registration.name}`);
throw error;
}
finally
{
await scope.dispose();
}
}
}
await this._logger.logInfo("Completed migrations.");
}
}
class MigrationRegistration
{
private readonly _name: string;
private readonly _version: number;
private readonly _migration: Function;
public get name(): string { return this._name; }
public get version(): number { return this._version; }
public get migration(): Function { return this._migration; }
public constructor(migration: Function)
{
given(migration, "migration").ensureHasValue().ensureIsFunction();
const migrationName = (<Object>migration).getTypeName();
const errorMessage = `invalid migration name ${migrationName}`;
given(migrationName, "migrationName").ensureHasValue().ensureIsString()
.ensure(t => t.contains("_"), errorMessage)
.ensure(t => t.split("_").length === 2, errorMessage)
.ensure(t => Number.parseInt(t.split("_")[1]) > 0, errorMessage);
this._name = migrationName;
this._version = Number.parseInt(migrationName.split("_")[1]);
this._migration = migration;
}
}