Merge branch 'migrate-dexie-to-sqlite'
This commit is contained in:
1373
src/services/indexedDBMigrationService.ts
Normal file
1373
src/services/indexedDBMigrationService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,150 @@
|
||||
/**
|
||||
* Manage database migrations as people upgrade their app over time
|
||||
*/
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Migration interface for database schema migrations
|
||||
*/
|
||||
interface Migration {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
|
||||
export class MigrationService {
|
||||
private static instance: MigrationService;
|
||||
/**
|
||||
* Migration registry to store and manage database migrations
|
||||
*/
|
||||
class MigrationRegistry {
|
||||
private migrations: Migration[] = [];
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): MigrationService {
|
||||
if (!MigrationService.instance) {
|
||||
MigrationService.instance = new MigrationService();
|
||||
}
|
||||
return MigrationService.instance;
|
||||
}
|
||||
|
||||
registerMigration(migration: Migration) {
|
||||
/**
|
||||
* Register a migration with the registry
|
||||
*
|
||||
* @param migration - The migration to register
|
||||
*/
|
||||
registerMigration(migration: Migration): void {
|
||||
this.migrations.push(migration);
|
||||
logger.info(`[MigrationService] Registered migration: ${migration.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
* Get all registered migrations
|
||||
*
|
||||
* @returns Array of registered migrations
|
||||
*/
|
||||
async runMigrations<T>(
|
||||
// note that this does not take parameters because the Capacitor SQLite 'execute' is different
|
||||
sqlExec: (sql: string) => Promise<unknown>,
|
||||
sqlQuery: (sql: string) => Promise<T>,
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
// Create migrations table if it doesn't exist
|
||||
await sqlExec(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
getMigrations(): Migration[] {
|
||||
return this.migrations;
|
||||
}
|
||||
|
||||
// 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}')`,
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear all registered migrations
|
||||
*/
|
||||
clearMigrations(): void {
|
||||
this.migrations = [];
|
||||
logger.info("[MigrationService] Cleared all registered migrations");
|
||||
}
|
||||
}
|
||||
|
||||
export default MigrationService.getInstance();
|
||||
// Create a singleton instance of the migration registry
|
||||
const migrationRegistry = new MigrationRegistry();
|
||||
|
||||
/**
|
||||
* Register a migration with the migration service
|
||||
*
|
||||
* This function is used by the migration system to register database
|
||||
* schema migrations that need to be applied to the database.
|
||||
*
|
||||
* @param migration - The migration to register
|
||||
*/
|
||||
export function registerMigration(migration: Migration): void {
|
||||
migrationRegistry.registerMigration(migration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all registered migrations against the database
|
||||
*
|
||||
* This function executes all registered migrations in order, checking
|
||||
* which ones have already been applied to avoid duplicate execution.
|
||||
* It creates a migrations table if it doesn't exist to track applied
|
||||
* migrations.
|
||||
*
|
||||
* @param sqlExec - Function to execute SQL statements
|
||||
* @param sqlQuery - Function to query SQL data
|
||||
* @param extractMigrationNames - Function to extract migration names from query results
|
||||
* @returns Promise that resolves when all migrations are complete
|
||||
*/
|
||||
export async function runMigrations<T>(
|
||||
sqlExec: (sql: string, params?: unknown[]) => Promise<unknown>,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Create migrations table if it doesn't exist
|
||||
await sqlExec(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Get list of already applied migrations
|
||||
const appliedMigrationsResult = await sqlQuery(
|
||||
"SELECT name FROM migrations",
|
||||
);
|
||||
const appliedMigrations = extractMigrationNames(appliedMigrationsResult);
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Found ${appliedMigrations.size} applied migrations`,
|
||||
);
|
||||
|
||||
// Get all registered migrations
|
||||
const migrations = migrationRegistry.getMigrations();
|
||||
|
||||
if (migrations.length === 0) {
|
||||
logger.warn("[MigrationService] No migrations registered");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Running ${migrations.length} registered migrations`,
|
||||
);
|
||||
|
||||
// Run each migration that hasn't been applied yet
|
||||
for (const migration of migrations) {
|
||||
if (appliedMigrations.has(migration.name)) {
|
||||
logger.info(
|
||||
`[MigrationService] Skipping already applied migration: ${migration.name}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`[MigrationService] Applying migration: ${migration.name}`);
|
||||
|
||||
try {
|
||||
// Execute the migration SQL
|
||||
await sqlExec(migration.sql);
|
||||
|
||||
// Record that the migration was applied
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Successfully applied migration: ${migration.name}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[MigrationService] Failed to apply migration ${migration.name}:`,
|
||||
error,
|
||||
);
|
||||
throw new Error(`Migration ${migration.name} failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[MigrationService] All migrations completed successfully");
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Migration process failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user