|
| 1 | +import 'package:flutter_news_app_api_server_full_source_code/src/database/migration.dart'; |
| 2 | +import 'package:logging/logging.dart'; |
| 3 | +import 'package:mongo_dart/mongo_dart.dart'; |
| 4 | + |
| 5 | +/// {@template database_migration_service} |
| 6 | +/// A service responsible for managing and executing database migrations. |
| 7 | +/// |
| 8 | +/// This service discovers, sorts, and applies pending [Migration] scripts |
| 9 | +/// to the MongoDB database. It tracks applied migrations in a dedicated |
| 10 | +/// `migrations_history` collection to ensure idempotency and prevent |
| 11 | +/// redundant execution. |
| 12 | +/// |
| 13 | +/// Migrations are identified by a unique version string (YYYYMMDDHHMMSS) |
| 14 | +/// and are always applied in chronological order. |
| 15 | +/// {@endtemplate} |
| 16 | +class DatabaseMigrationService { |
| 17 | + /// {@macro database_migration_service} |
| 18 | + DatabaseMigrationService({ |
| 19 | + required Db db, |
| 20 | + required Logger log, |
| 21 | + required List<Migration> migrations, |
| 22 | + }) : _db = db, |
| 23 | + _log = log, |
| 24 | + _migrations = migrations; |
| 25 | + |
| 26 | + final Db _db; |
| 27 | + final Logger _log; |
| 28 | + final List<Migration> _migrations; |
| 29 | + |
| 30 | + /// The name of the MongoDB collection used to track applied migrations. |
| 31 | + static const String _migrationsCollectionName = 'migrations_history'; |
| 32 | + |
| 33 | + /// Initializes the migration service and applies any pending migrations. |
| 34 | + /// |
| 35 | + /// This method performs the following steps: |
| 36 | + /// 1. Ensures the `migrations_history` collection exists and has a unique |
| 37 | + /// index on the `version` field. |
| 38 | + /// 2. Fetches all previously applied migration versions from the database. |
| 39 | + /// 3. Sorts the registered migrations by their version string. |
| 40 | + /// 4. Iterates through the sorted migrations, applying only those that |
| 41 | + /// have not yet been applied. |
| 42 | + /// 5. Records each successfully applied migration in the `migrations_history` |
| 43 | + /// collection. |
| 44 | + Future<void> init() async { |
| 45 | + _log.info('Starting database migration process...'); |
| 46 | + |
| 47 | + await _ensureMigrationsCollectionAndIndex(); |
| 48 | + |
| 49 | + final appliedVersions = await _getAppliedMigrationVersions(); |
| 50 | + _log.fine('Applied migration versions: $appliedVersions'); |
| 51 | + |
| 52 | + // Sort migrations by version to ensure chronological application. |
| 53 | + _migrations.sort((a, b) => a.version.compareTo(b.version)); |
| 54 | + |
| 55 | + for (final migration in _migrations) { |
| 56 | + if (!appliedVersions.contains(migration.version)) { |
| 57 | + _log.info( |
| 58 | + 'Applying migration V${migration.version}: ${migration.description}', |
| 59 | + ); |
| 60 | + try { |
| 61 | + await migration.up(_db, _log); |
| 62 | + await _recordMigration(migration.version); |
| 63 | + _log.info( |
| 64 | + 'Successfully applied migration V${migration.version}.', |
| 65 | + ); |
| 66 | + } catch (e, s) { |
| 67 | + _log.severe( |
| 68 | + 'Failed to apply migration V${migration.version}: ' |
| 69 | + '${migration.description}', |
| 70 | + e, |
| 71 | + s, |
| 72 | + ); |
| 73 | + // Re-throw to halt application startup if a migration fails. |
| 74 | + rethrow; |
| 75 | + } |
| 76 | + } else { |
| 77 | + _log.fine( |
| 78 | + 'Migration V${migration.version} already applied. Skipping.', |
| 79 | + ); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + _log.info('Database migration process completed.'); |
| 84 | + } |
| 85 | + |
| 86 | + /// Ensures the `migrations_history` collection exists and has a unique index |
| 87 | + /// on the `version` field. |
| 88 | + Future<void> _ensureMigrationsCollectionAndIndex() async { |
| 89 | + _log.fine('Ensuring migrations_history collection and index...'); |
| 90 | + final collection = _db.collection(_migrationsCollectionName); |
| 91 | + await collection.createIndex( |
| 92 | + key: 'version', |
| 93 | + unique: true, |
| 94 | + name: 'version_unique_index', |
| 95 | + ); |
| 96 | + _log.fine('Migrations_history collection and index ensured.'); |
| 97 | + } |
| 98 | + |
| 99 | + /// Retrieves a set of versions of all migrations that have already been |
| 100 | + /// applied to the database. |
| 101 | + Future<Set<String>> _getAppliedMigrationVersions() async { |
| 102 | + final collection = _db.collection(_migrationsCollectionName); |
| 103 | + final documents = await collection.find().toList(); |
| 104 | + return documents |
| 105 | + .map((doc) => doc['version'] as String) |
| 106 | + .toSet(); |
| 107 | + } |
| 108 | + |
| 109 | + /// Records a successfully applied migration in the `migrations_history` |
| 110 | + /// collection. |
| 111 | + Future<void> _recordMigration(String version) async { |
| 112 | + final collection = _db.collection(_migrationsCollectionName); |
| 113 | + await collection.insertOne({ |
| 114 | + 'version': version, |
| 115 | + 'appliedAt': DateTime.now().toUtc(), |
| 116 | + }); |
| 117 | + } |
| 118 | +} |
0 commit comments