-
Notifications
You must be signed in to change notification settings - Fork 0
Feature document database migration #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
49d1e91
feat(auth): create ownership check middleware
fulleni ea709e9
feat(auth): apply ownership check middleware to item routes
fulleni cc9d32f
refactor(api): adapt GET /data to use native document query
fulleni 34a0029
refactor(api): adapt GET /data to use native document query
fulleni c065be4
feat(db): replace postgres seeding service with mongodb
fulleni fe51a1e
feat(db): replace postgres seeding service with mongodb
fulleni 1bca9ab
refactor(deps): remove postgres dependencies
fulleni bfb3e5f
feat(db): migrate dependency injection to mongodb
fulleni bf6f59d
docs: update readme for mongodb migration
fulleni c6877bc
refactor(auth): use readAll with filter in AuthService
fulleni 38853ea
docs: update .env.example for mongodb connection string
fulleni b07d6d2
fix(config): make .env file loading robust
fulleni 7b24b53
/// issues where the execution context's working directory is not the
fulleni 20a92d2
fix(config): correct directory traversal in .env search
fulleni 96912c2
feat(config): enhance .env loading logs
fulleni e7d0d8d
fix(config): prevent state corruption on failed dependency init
fulleni d1ac01a
fix(config): make dependency init robust against all throwables
fulleni 6e84bcf
chore
fulleni 4ce3cc5
fix(api): make database seeding idempotent and preserve IDs
fulleni 5207475
fix(api): ensure all user data is deleted on account deletion
fulleni dbae817
refactor(api): use repository count method for dashboard summary
fulleni c49d2c4
fix(api): improve error handling for invalid JSON body
fulleni b2b4363
fix(api): correct admin data scoping in generic data handlers
fulleni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(db): replace postgres seeding service with mongodb
- Rewrites `DatabaseSeedingService` to work with a MongoDB `Db` instance. - Removes table creation logic, as MongoDB collections are schemaless. - Implements idempotent seeding using `bulkWrite` with `upsert` operations, preventing duplicate data on subsequent runs. - Correctly handles the conversion of string IDs from fixtures to MongoDB `ObjectId` for the `_id` field. - Ensures complex nested objects in fixtures are properly JSON-encoded before insertion.
- Loading branch information
There are no files selected for viewing
160 changes: 64 additions & 96 deletions
lib/src/services/database_seeding_service.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,149 +1,117 @@ | ||
| import 'dart:convert'; | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:ht_shared/ht_shared.dart'; | ||
| import 'package:logging/logging.dart'; | ||
| import 'package:mongo_dart/mongo_dart.dart'; | ||
|
|
||
| /// {@template database_seeding_service} | ||
| /// A service responsible for seeding the MongoDB database with initial data. | ||
| /// | ||
| /// This service reads data from local JSON fixture files and uses `upsert` | ||
| /// operations to ensure that the seeding process is idempotent. It can be | ||
| /// run multiple times without creating duplicate documents. | ||
| /// This service reads data from predefined fixture lists in `ht_shared` and | ||
| /// uses `upsert` operations to ensure that the seeding process is idempotent. | ||
| /// It can be run multiple times without creating duplicate documents. | ||
| /// {@endtemplate} | ||
| class DatabaseSeedingService { | ||
| /// {@macro database_seeding_service} | ||
| const DatabaseSeedingService({required Db db, required Logger log}) | ||
| : _db = db, | ||
| _log = log; | ||
| : _db = db, | ||
| _log = log; | ||
|
|
||
| final Db _db; | ||
| final Logger _log; | ||
|
|
||
| /// The main entry point for seeding all necessary data. | ||
| Future<void> seedInitialData() async { | ||
| _log.info('Starting database seeding process...'); | ||
| await _seedCollection( | ||
|
|
||
| await _seedCollection<Country>( | ||
| collectionName: 'countries', | ||
| fixturePath: 'lib/src/fixtures/countries.json', | ||
| fixtureData: countriesFixturesData, | ||
| getId: (item) => item.id, | ||
| toJson: (item) => item.toJson(), | ||
| ); | ||
| await _seedCollection( | ||
| await _seedCollection<Source>( | ||
| collectionName: 'sources', | ||
| fixturePath: 'lib/src/fixtures/sources.json', | ||
| fixtureData: sourcesFixturesData, | ||
| getId: (item) => item.id, | ||
| toJson: (item) => item.toJson(), | ||
| ); | ||
| await _seedCollection( | ||
| await _seedCollection<Topic>( | ||
| collectionName: 'topics', | ||
| fixturePath: 'lib/src/fixtures/topics.json', | ||
| fixtureData: topicsFixturesData, | ||
| getId: (item) => item.id, | ||
| toJson: (item) => item.toJson(), | ||
| ); | ||
| await _seedCollection( | ||
| await _seedCollection<Headline>( | ||
| collectionName: 'headlines', | ||
| fixturePath: 'lib/src/fixtures/headlines.json', | ||
| fixtureData: headlinesFixturesData, | ||
| getId: (item) => item.id, | ||
| toJson: (item) => item.toJson(), | ||
| ); | ||
| await _seedCollection<User>( | ||
| collectionName: 'users', | ||
| fixtureData: usersFixturesData, | ||
| getId: (item) => item.id, | ||
| toJson: (item) => item.toJson(), | ||
| ); | ||
| await _seedCollection<RemoteConfig>( | ||
| collectionName: 'remote_configs', | ||
| fixtureData: remoteConfigsFixturesData, | ||
| getId: (item) => item.id, | ||
| toJson: (item) => item.toJson(), | ||
| ); | ||
| await _seedInitialAdminAndConfig(); | ||
|
|
||
| _log.info('Database seeding process completed.'); | ||
| } | ||
|
|
||
| /// Seeds a specific collection from a given JSON fixture file. | ||
| Future<void> _seedCollection({ | ||
| /// Seeds a specific collection from a given list of fixture data. | ||
| Future<void> _seedCollection<T>({ | ||
| required String collectionName, | ||
| required String fixturePath, | ||
| required List<T> fixtureData, | ||
| required String Function(T) getId, | ||
| required Map<String, dynamic> Function(T) toJson, | ||
| }) async { | ||
| _log.info('Seeding collection: "$collectionName" from "$fixturePath"...'); | ||
| _log.info('Seeding collection: "$collectionName"...'); | ||
| try { | ||
| final collection = _db.collection(collectionName); | ||
| final file = File(fixturePath); | ||
| if (!await file.exists()) { | ||
| _log.warning('Fixture file not found: $fixturePath. Skipping.'); | ||
| return; | ||
| } | ||
|
|
||
| final content = await file.readAsString(); | ||
| final documents = jsonDecode(content) as List<dynamic>; | ||
|
|
||
| if (documents.isEmpty) { | ||
| if (fixtureData.isEmpty) { | ||
| _log.info('No documents to seed for "$collectionName".'); | ||
| return; | ||
| } | ||
|
|
||
| final bulk = collection.initializeUnorderedBulkOperation(); | ||
| final collection = _db.collection(collectionName); | ||
| final operations = <Map<String, Object>>[]; | ||
|
|
||
| for (final doc in documents) { | ||
| final docMap = doc as Map<String, dynamic>; | ||
| final id = docMap['id'] as String?; | ||
| for (final item in fixtureData) { | ||
| final id = getId(item); | ||
|
|
||
| if (id == null || !ObjectId.isValidHexId(id)) { | ||
| _log.warning('Skipping document with invalid or missing ID: $doc'); | ||
| if (!ObjectId.isValidHexId(id)) { | ||
| _log.warning('Skipping document with invalid ID format: $id'); | ||
| continue; | ||
| } | ||
|
|
||
| final objectId = ObjectId.fromHexString(id); | ||
| // Remove the string 'id' field and use '_id' with ObjectId | ||
| docMap.remove('id'); | ||
| final document = toJson(item)..remove('id'); | ||
|
|
||
| operations.add({ | ||
| 'replaceOne': { | ||
| 'filter': {'_id': objectId}, | ||
| 'replacement': document, | ||
| 'upsert': true, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| bulk.find({'_id': objectId}).upsert().replaceOne(docMap); | ||
| if (operations.isEmpty) { | ||
| _log.info('No valid documents to write for "$collectionName".'); | ||
| return; | ||
| } | ||
|
|
||
| final result = await bulk.execute(); | ||
| final result = await collection.bulkWrite(operations); | ||
| _log.info( | ||
| 'Seeding for "$collectionName" complete. ' | ||
| 'Upserted: ${result.nUpserted}, Modified: ${result.nModified}.', | ||
| ); | ||
| } on Exception catch (e, s) { | ||
| _log.severe( | ||
| 'Failed to seed collection "$collectionName" from "$fixturePath".', | ||
| e, | ||
| s, | ||
| ); | ||
| // Re-throwing to halt the startup process if seeding fails. | ||
| rethrow; | ||
| } | ||
| } | ||
|
|
||
| /// Seeds the initial admin user and remote config document. | ||
| Future<void> _seedInitialAdminAndConfig() async { | ||
| _log.info('Seeding initial admin user and remote config...'); | ||
| try { | ||
| // --- Seed Admin User --- | ||
| final usersCollection = _db.collection('users'); | ||
| final adminUser = User.fromJson(adminUserFixture); | ||
| final adminDoc = adminUser.toJson() | ||
| ..['app_role'] = adminUser.appRole.name | ||
| ..['dashboard_role'] = adminUser.dashboardRole.name | ||
| ..['feed_action_status'] = jsonEncode(adminUser.feedActionStatus) | ||
| ..remove('id'); | ||
|
|
||
| await usersCollection.updateOne( | ||
| where.id(ObjectId.fromHexString(adminUser.id)), | ||
| modify.set( | ||
| 'email', | ||
| adminDoc['email'], | ||
| ).setAll(adminDoc), // Use setAll to add/update all fields | ||
| upsert: true, | ||
| ); | ||
| _log.info('Admin user seeded successfully.'); | ||
|
|
||
| // --- Seed Remote Config --- | ||
| final remoteConfigCollection = _db.collection('remote_config'); | ||
| final remoteConfig = RemoteConfig.fromJson(remoteConfigFixture); | ||
| final remoteConfigDoc = remoteConfig.toJson() | ||
| ..['user_preference_limits'] = | ||
| jsonEncode(remoteConfig.userPreferenceConfig.toJson()) | ||
| ..['ad_config'] = jsonEncode(remoteConfig.adConfig.toJson()) | ||
| ..['account_action_config'] = | ||
| jsonEncode(remoteConfig.accountActionConfig.toJson()) | ||
| ..['app_status'] = jsonEncode(remoteConfig.appStatus.toJson()) | ||
| ..remove('id'); | ||
|
|
||
| await remoteConfigCollection.updateOne( | ||
| where.id(ObjectId.fromHexString(remoteConfig.id)), | ||
| modify.setAll(remoteConfigDoc), | ||
| upsert: true, | ||
| ); | ||
| _log.info('Remote config seeded successfully.'); | ||
| } on Exception catch (e, s) { | ||
| _log.severe('Failed to seed admin user or remote config.', e, s); | ||
| _log.severe('Failed to seed collection "$collectionName".', e, s); | ||
| rethrow; | ||
| } | ||
| } | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.