interface Migration { name: string; sql: string; } export class MigrationService { private static instance: MigrationService; private migrations: Migration[] = []; private constructor() {} static getInstance(): MigrationService { if (!MigrationService.instance) { MigrationService.instance = new MigrationService(); } return MigrationService.instance; } registerMigration(migration: Migration) { this.migrations.push(migration); } /** * @param sqlExec - A function that executes a SQL statement and returns some update result * @param sqlQuery - A function that executes a SQL query and returns the result in some format * @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query */ async runMigrations( // note that this does not take parameters because the Capacitor SQLite 'execute' is different sqlExec: (sql: string) => Promise, sqlQuery: (sql: string) => Promise, extractMigrationNames: (result: T) => Set, ): Promise { // Create migrations table if it doesn't exist const result0 = await sqlExec(` CREATE TABLE IF NOT EXISTS migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `); // Get list of executed migrations const result1: T = await sqlQuery("SELECT name FROM migrations;"); const executedMigrations = extractMigrationNames(result1); // Run pending migrations in order for (const migration of this.migrations) { if (!executedMigrations.has(migration.name)) { await sqlExec(migration.sql); await sqlExec( `INSERT INTO migrations (name) VALUES ('${migration.name}')`, ); } } } } export default MigrationService.getInstance();