-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor/sync with core update #94
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 37 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
c3acbc1
build(deps): update core package reference
fulleni 8bcffea
feat(rbac): define permissions for interest and in-app notification m…
fulleni d37b3cd
feat(rbac): grant interest and notification permissions to app roles
fulleni 3795ac8
feat(registry): add permission configuration for interests and notifi…
fulleni bf67080
feat(database): unify interests and remote config
fulleni 35846b1
feat(database): add new migration to unify interests and remote config
fulleni ca1decf
feat(dependencies): add repositories for Interest and InAppNotification
fulleni 944240d
refactor(service): update user preference limit logic
fulleni 9a8d5c1
refactor(push-notification): replace subscriptions with interests in …
fulleni b800952
fix(auth_service): update user preferences initialization
fulleni 1875789
feat(database): add indexes for interests and in-app notifications
fulleni 4bf79d7
feat(data): add interest and in-app notification support
fulleni 79d0f1d
feat(routes): add middleware providers for Interest and InAppNotifica…
fulleni 115204f
feat(firebase): enhance logging for push notification batches
fulleni b629b5a
docs(core): update abstract class documentation
fulleni f9f11e2
docs(auth): add documentation comments to FirebaseAuthenticator
fulleni 64e9236
refactor(push-notification): enhance _sendBatch function with additio…
fulleni b282423
style: fix
fulleni 10ab766
refactor(api)!: re-implement DefaultUserPreferenceLimitService
fulleni 5d61539
fix(api): update data operation registry to use new limit services
fulleni 11403f0
refactor(dependencies): remove permissionService from AppDependencies
fulleni 93562d0
fix(api): remove obsolete validation call in PUT handler
fulleni db55ea2
refactor(dependencies): remove interest repository and update push no…
fulleni 8774c2e
refactor(rbac): remove interest permissions
fulleni b798ffd
fix(rbac): remove self-manage interests permissions for app guests
fulleni 64d3373
refactor(data): remove interest model and related operations
fulleni 918de42
refactor(database): remove indexes creation for interests collection
fulleni 2cf110b
refactor(push-notification): update breaking news subscription query
fulleni 9c3948b
refactor(routes): remove unused interestRepository provider
fulleni 9784272
fix(database): handle malformed criteria in saved filters and subscri…
fulleni 935a531
style: format
fulleni 14b8c5e
docs(README): enhance notification streams description
fulleni 39c471a
refactor(api): unify user preference limit checking
fulleni aaae462
refactor: remove unused import
fulleni 8a45a96
fix(api): harden migration script and correctly implement admin limit…
fulleni 553a00a
fix(api): correct critical bug in preference limit validation
fulleni 0e49173
style(firebase_auth): remove outdated comment
fulleni acb356e
refactor(api): remove unused currentPreferences from limit service
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
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
229 changes: 229 additions & 0 deletions
229
lib/src/database/migrations/20251111000000_unify_interests_and_remote_config.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 |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| import 'package:core/core.dart'; | ||
| import 'package:flutter_news_app_api_server_full_source_code/src/database/migration.dart'; | ||
| import 'package:logging/logging.dart'; | ||
| import 'package:mongo_dart/mongo_dart.dart'; | ||
|
|
||
| /// {@template unify_interests_and_remote_config} | ||
| /// A migration to refactor the database schema by unifying `SavedFilter` and | ||
| /// `PushNotificationSubscription` into a single `Interest` model. | ||
| /// | ||
| /// This migration performs two critical transformations: | ||
| /// | ||
| /// 1. **User Preferences Transformation:** It iterates through all | ||
| /// `user_content_preferences` documents. For each user, it reads the | ||
| /// legacy `savedFilters` and `notificationSubscriptions` arrays, converts | ||
| /// them into the new `Interest` format, and merges them. It then saves | ||
| /// this new list to an `interests` field and removes the old, obsolete | ||
| /// arrays. | ||
| /// | ||
| /// 2. **Remote Config Transformation:** It updates the single `remote_configs` | ||
| /// document by adding the new `interestConfig` field with default limits | ||
| /// and removing the now-deprecated limit fields from `userPreferenceConfig` | ||
| /// and `pushNotificationConfig`. | ||
| /// {@endtemplate} | ||
| class UnifyInterestsAndRemoteConfig extends Migration { | ||
| /// {@macro unify_interests_and_remote_config} | ||
| UnifyInterestsAndRemoteConfig() | ||
| : super( | ||
| prDate: '20251111000000', | ||
| prId: '74', | ||
| prSummary: | ||
| 'This pull request introduces a significant new Interest feature, designed to enhance user personalization by unifying content filtering and notification subscriptions.', | ||
| ); | ||
|
|
||
| @override | ||
| Future<void> up(Db db, Logger log) async { | ||
| log.info('Starting migration: UnifyInterestsAndRemoteConfig.up'); | ||
|
|
||
| // --- 1. Migrate user_content_preferences --- | ||
| log.info('Migrating user_content_preferences collection...'); | ||
| final preferencesCollection = db.collection('user_content_preferences'); | ||
| final allPreferences = await preferencesCollection.find().toList(); | ||
|
|
||
| for (final preferenceDoc in allPreferences) { | ||
| final userId = (preferenceDoc['_id'] as ObjectId).oid; | ||
| log.finer('Processing preferences for user: $userId'); | ||
|
|
||
| final savedFilters = | ||
| (preferenceDoc['savedFilters'] as List<dynamic>? ?? []) | ||
| .map((e) => e as Map<String, dynamic>) | ||
| .toList(); | ||
| final notificationSubscriptions = | ||
| (preferenceDoc['notificationSubscriptions'] as List<dynamic>? ?? []) | ||
| .map((e) => e as Map<String, dynamic>) | ||
| .toList(); | ||
|
|
||
| if (savedFilters.isEmpty && notificationSubscriptions.isEmpty) { | ||
| log.finer('User $userId has no legacy data to migrate. Skipping.'); | ||
| continue; | ||
| } | ||
|
|
||
| // Use a map to merge filters and subscriptions with the same criteria. | ||
| final interestMap = <String, Interest>{}; | ||
|
|
||
| // Process saved filters | ||
| for (final filter in savedFilters) { | ||
| final criteriaData = filter['criteria']; | ||
| if (criteriaData is! Map<String, dynamic>) { | ||
| log.warning( | ||
| 'User $userId has a malformed savedFilter with missing or invalid ' | ||
| '"criteria". Skipping this filter.', | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| final criteria = InterestCriteria.fromJson(criteriaData); | ||
| final key = _generateCriteriaKey(criteria); | ||
|
|
||
| interestMap.update( | ||
| key, | ||
| (existing) => existing.copyWith(isPinnedFeedFilter: true), | ||
| ifAbsent: () => Interest( | ||
| id: ObjectId().oid, | ||
| userId: userId, | ||
| name: filter['name'] as String, | ||
| criteria: criteria, | ||
| isPinnedFeedFilter: true, | ||
| deliveryTypes: const {}, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| // Process notification subscriptions | ||
| for (final subscription in notificationSubscriptions) { | ||
| final criteriaData = subscription['criteria']; | ||
| if (criteriaData is! Map<String, dynamic>) { | ||
| log.warning( | ||
| 'User $userId has a malformed notificationSubscription with ' | ||
| 'missing or invalid "criteria". Skipping this subscription.', | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| final criteria = InterestCriteria.fromJson(criteriaData); | ||
| final key = _generateCriteriaKey(criteria); | ||
| final deliveryTypes = | ||
| (subscription['deliveryTypes'] as List<dynamic>? ?? []) | ||
| .map((e) { | ||
| try { | ||
| return PushNotificationSubscriptionDeliveryType.values | ||
| .byName(e as String); | ||
| } catch (_) { | ||
| log.warning( | ||
| 'User $userId has a notificationSubscription with an invalid deliveryType: "$e". Skipping this type.', | ||
| ); | ||
| return null; | ||
| } | ||
| }) | ||
| .whereType<PushNotificationSubscriptionDeliveryType>() | ||
| .toSet(); | ||
|
|
||
| interestMap.update( | ||
| key, | ||
| (existing) => existing.copyWith( | ||
| deliveryTypes: {...existing.deliveryTypes, ...deliveryTypes}, | ||
| ), | ||
| ifAbsent: () => Interest( | ||
| id: ObjectId().oid, | ||
| userId: userId, | ||
| name: subscription['name'] as String, | ||
| criteria: criteria, | ||
| isPinnedFeedFilter: false, | ||
| deliveryTypes: deliveryTypes, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| final newInterests = interestMap.values.map((i) => i.toJson()).toList(); | ||
|
|
||
| await preferencesCollection.updateOne( | ||
| where.id(preferenceDoc['_id'] as ObjectId), | ||
| modify | ||
| .set('interests', newInterests) | ||
| .unset('savedFilters') | ||
| .unset('notificationSubscriptions'), | ||
| ); | ||
| log.info( | ||
| 'Successfully migrated ${newInterests.length} interests for user $userId.', | ||
| ); | ||
| } | ||
|
|
||
| // --- 2. Migrate remote_configs --- | ||
| log.info('Migrating remote_configs collection...'); | ||
| final remoteConfigCollection = db.collection('remote_configs'); | ||
| final remoteConfig = await remoteConfigCollection.findOne(); | ||
|
|
||
| if (remoteConfig != null) { | ||
| // Use the default from the core package fixtures as the base. | ||
| final defaultConfig = remoteConfigsFixturesData.first.interestConfig; | ||
|
|
||
| await remoteConfigCollection.updateOne( | ||
| where.id(remoteConfig['_id'] as ObjectId), | ||
| modify | ||
| .set('interestConfig', defaultConfig.toJson()) | ||
| .unset('userPreferenceConfig.guestSavedFiltersLimit') | ||
| .unset('userPreferenceConfig.authenticatedSavedFiltersLimit') | ||
| .unset('userPreferenceConfig.premiumSavedFiltersLimit') | ||
| .unset('pushNotificationConfig.deliveryConfigs'), | ||
| ); | ||
| log.info('Successfully migrated remote_configs document.'); | ||
| } else { | ||
| log.warning('Remote config document not found. Skipping migration.'); | ||
| } | ||
|
|
||
| log.info('Migration UnifyInterestsAndRemoteConfig.up completed.'); | ||
| } | ||
|
|
||
| @override | ||
| Future<void> down(Db db, Logger log) async { | ||
| log.warning( | ||
| 'Executing "down" for UnifyInterestsAndRemoteConfig. ' | ||
| 'This is a destructive operation and may result in data loss.', | ||
| ); | ||
|
|
||
| // --- 1. Revert user_content_preferences --- | ||
| final preferencesCollection = db.collection('user_content_preferences'); | ||
| await preferencesCollection.updateMany( | ||
| where.exists('interests'), | ||
| modify | ||
| .unset('interests') | ||
| .set('savedFilters', <dynamic>[]) | ||
| .set('notificationSubscriptions', <dynamic>[]), | ||
| ); | ||
| log.info( | ||
| 'Removed "interests" field and re-added empty legacy fields to all ' | ||
| 'user_content_preferences documents.', | ||
| ); | ||
|
|
||
| // --- 2. Revert remote_configs --- | ||
| final remoteConfigCollection = db.collection('remote_configs'); | ||
| await remoteConfigCollection.updateMany( | ||
| where.exists('interestConfig'), | ||
| modify | ||
| .unset('interestConfig') | ||
| .set('userPreferenceConfig.guestSavedFiltersLimit', 5) | ||
| .set('userPreferenceConfig.authenticatedSavedFiltersLimit', 20) | ||
| .set('userPreferenceConfig.premiumSavedFiltersLimit', 50) | ||
| .set( | ||
| 'pushNotificationConfig.deliveryConfigs', | ||
| { | ||
| 'breakingOnly': true, | ||
| 'dailyDigest': true, | ||
| 'weeklyRoundup': true, | ||
| }, | ||
| ), | ||
fulleni marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
| log.info('Reverted remote_configs document to legacy structure.'); | ||
|
|
||
| log.info('Migration UnifyInterestsAndRemoteConfig.down completed.'); | ||
| } | ||
|
|
||
| /// Generates a stable, sorted key from interest criteria to identify | ||
| /// duplicates. | ||
| String _generateCriteriaKey(InterestCriteria criteria) { | ||
| final topics = criteria.topics.map((t) => t.id).toList()..sort(); | ||
| final sources = criteria.sources.map((s) => s.id).toList()..sort(); | ||
| final countries = criteria.countries.map((c) => c.id).toList()..sort(); | ||
| return 't:${topics.join(',')};s:${sources.join(',')};c:${countries.join(',')}'; | ||
| } | ||
| } | ||
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.