Skip to content

Commit 2651006

Browse files
committed
feat(database): implement database migration service
- Add DatabaseMigrationService to manage and execute database migrations - Implement migration process: ensure migrations_history collection, fetch applied versions, sort migrations, apply pending migrations, record applied migrations - Use unique index on version field for idempotency and prevent redundant execution - Log migration process and handle failures
1 parent f447df9 commit 2651006

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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

Comments
 (0)