Skip to content

Commit bfb3e5f

Browse files
committed
feat(db): migrate dependency injection to mongodb
- Rewrites `AppDependencies` to use `MongoDbConnectionManager` instead of the PostgreSQL equivalent. - Initializes the MongoDB connection and runs the new `DatabaseSeedingService`. - Instantiates `HtDataMongodb` clients for all data models. - Wires up all `HtDataRepository` instances and application services to use the new MongoDB-backed data layer. - This completes the core dependency injection part of the migration.
1 parent 1bca9ab commit bfb3e5f

File tree

1 file changed

+116
-203
lines changed

1 file changed

+116
-203
lines changed

lib/src/config/app_dependencies.dart

Lines changed: 116 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import 'dart:async';
2-
import 'dart:convert';
3-
4-
import 'package:ht_api/src/config/database_connection.dart';
1+
import 'package:ht_api/src/config/environment_config.dart';
52
import 'package:ht_api/src/rbac/permission_service.dart';
63
import 'package:ht_api/src/services/auth_service.dart';
74
import 'package:ht_api/src/services/auth_token_service.dart';
@@ -12,225 +9,162 @@ import 'package:ht_api/src/services/jwt_auth_token_service.dart';
129
import 'package:ht_api/src/services/token_blacklist_service.dart';
1310
import 'package:ht_api/src/services/user_preference_limit_service.dart';
1411
import 'package:ht_api/src/services/verification_code_storage_service.dart';
15-
import 'package:ht_data_client/ht_data_client.dart';
16-
import 'package:ht_data_postgres/ht_data_postgres.dart';
12+
import 'package:ht_data_mongodb/ht_data_mongodb.dart';
1713
import 'package:ht_data_repository/ht_data_repository.dart';
1814
import 'package:ht_email_inmemory/ht_email_inmemory.dart';
1915
import 'package:ht_email_repository/ht_email_repository.dart';
2016
import 'package:ht_shared/ht_shared.dart';
2117
import 'package:logging/logging.dart';
22-
import 'package:postgres/postgres.dart';
2318
import 'package:uuid/uuid.dart';
2419

25-
/// A singleton class to manage all application dependencies.
26-
///
27-
/// This class follows a lazy initialization pattern. Dependencies are created
28-
/// only when the `init()` method is first called, typically triggered by the
29-
/// first incoming request. A `Completer` ensures that subsequent requests
30-
/// await the completion of the initial setup.
20+
/// {@template app_dependencies}
21+
/// A singleton class responsible for initializing and providing all application
22+
/// dependencies, such as database connections, repositories, and services.
23+
/// {@endtemplate}
3124
class AppDependencies {
25+
/// Private constructor for the singleton pattern.
3226
AppDependencies._();
3327

34-
/// The single, global instance of the [AppDependencies].
35-
static final instance = AppDependencies._();
28+
/// The single, static instance of this class.
29+
static final AppDependencies _instance = AppDependencies._();
30+
31+
/// Provides access to the singleton instance.
32+
static AppDependencies get instance => _instance;
3633

34+
bool _isInitialized = false;
3735
final _log = Logger('AppDependencies');
38-
final _completer = Completer<void>();
3936

40-
// --- Repositories ---
41-
/// A repository for managing [Headline] data.
42-
late final HtDataRepository<Headline> headlineRepository;
37+
// --- Late-initialized fields for all dependencies ---
4338

44-
/// A repository for managing [Topic] data.
45-
late final HtDataRepository<Topic> topicRepository;
39+
// Database
40+
late final MongoDbConnectionManager _mongoDbConnectionManager;
4641

47-
/// A repository for managing [Source] data.
42+
// Repositories
43+
late final HtDataRepository<Headline> headlineRepository;
44+
late final HtDataRepository<Topic> topicRepository;
4845
late final HtDataRepository<Source> sourceRepository;
49-
50-
/// A repository for managing [Country] data.
5146
late final HtDataRepository<Country> countryRepository;
52-
53-
/// A repository for managing [User] data.
5447
late final HtDataRepository<User> userRepository;
55-
56-
/// A repository for managing [UserAppSettings] data.
5748
late final HtDataRepository<UserAppSettings> userAppSettingsRepository;
58-
59-
/// A repository for managing [UserContentPreferences] data.
6049
late final HtDataRepository<UserContentPreferences>
61-
userContentPreferencesRepository;
62-
63-
/// A repository for managing the global [RemoteConfig] data.
50+
userContentPreferencesRepository;
6451
late final HtDataRepository<RemoteConfig> remoteConfigRepository;
65-
66-
// --- Services ---
67-
/// A service for sending emails.
6852
late final HtEmailRepository emailRepository;
6953

70-
/// A service for managing a blacklist of invalidated authentication tokens.
54+
// Services
7155
late final TokenBlacklistService tokenBlacklistService;
72-
73-
/// A service for generating and validating authentication tokens.
7456
late final AuthTokenService authTokenService;
75-
76-
/// A service for storing and validating one-time verification codes.
7757
late final VerificationCodeStorageService verificationCodeStorageService;
78-
79-
/// A service that orchestrates authentication logic.
8058
late final AuthService authService;
81-
82-
/// A service for calculating and providing a summary for the dashboard.
8359
late final DashboardSummaryService dashboardSummaryService;
84-
85-
/// A service for checking user permissions.
8660
late final PermissionService permissionService;
87-
88-
/// A service for enforcing limits on user content preferences.
8961
late final UserPreferenceLimitService userPreferenceLimitService;
9062

9163
/// Initializes all application dependencies.
9264
///
93-
/// This method is idempotent. It performs the full initialization only on
94-
/// the first call. Subsequent calls will await the result of the first one.
95-
Future<void> init() {
96-
if (_completer.isCompleted) {
97-
_log.fine('Dependencies already initializing/initialized.');
98-
return _completer.future;
99-
}
65+
/// This method is idempotent; it will only run the initialization logic once.
66+
Future<void> init() async {
67+
if (_isInitialized) return;
10068

10169
_log.info('Initializing application dependencies...');
102-
_init()
103-
.then((_) {
104-
_log.info('Application dependencies initialized successfully.');
105-
_completer.complete();
106-
})
107-
.catchError((Object e, StackTrace s) {
108-
_log.severe('Failed to initialize application dependencies.', e, s);
109-
_completer.completeError(e, s);
110-
});
111-
112-
return _completer.future;
113-
}
11470

115-
Future<void> _init() async {
116-
// 1. Establish Database Connection.
117-
await DatabaseConnectionManager.instance.init();
118-
final connection = await DatabaseConnectionManager.instance.connection;
71+
// 1. Initialize Database Connection
72+
_mongoDbConnectionManager = MongoDbConnectionManager();
73+
await _mongoDbConnectionManager.init(EnvironmentConfig.databaseUrl);
74+
_log.info('MongoDB connection established.');
11975

120-
// 2. Run Database Seeding.
76+
// 2. Seed Database
12177
final seedingService = DatabaseSeedingService(
122-
connection: connection,
123-
log: _log,
78+
db: _mongoDbConnectionManager.db,
79+
log: Logger('DatabaseSeedingService'),
12480
);
125-
await seedingService.createTables();
126-
await seedingService.seedGlobalFixtureData();
127-
await seedingService.seedInitialAdminAndConfig();
128-
129-
// 3. Initialize Repositories.
130-
headlineRepository = _createRepository(
131-
connection,
132-
'headlines',
133-
// The HtDataPostgresClient returns DateTime objects from TIMESTAMPTZ
134-
// columns. The Headline.fromJson factory expects ISO 8601 strings.
135-
// This handler converts them before deserialization.
136-
(json) => Headline.fromJson(_convertTimestampsToString(json)),
137-
(headline) => headline.toJson()
138-
..['source_id'] = headline.source.id
139-
..['topic_id'] = headline.topic.id
140-
..['event_country_id'] = headline.eventCountry.id
141-
..remove('source')
142-
..remove('topic')
143-
..remove('eventCountry'),
81+
await seedingService.seedInitialData();
82+
_log.info('Database seeding complete.');
83+
84+
// 3. Initialize Data Clients (MongoDB implementation)
85+
final headlineClient = HtDataMongodb<Headline>(
86+
connectionManager: _mongoDbConnectionManager,
87+
modelName: 'headlines',
88+
fromJson: Headline.fromJson,
89+
toJson: (item) => item.toJson(),
90+
logger: Logger('HtDataMongodb<Headline>'),
91+
);
92+
final topicClient = HtDataMongodb<Topic>(
93+
connectionManager: _mongoDbConnectionManager,
94+
modelName: 'topics',
95+
fromJson: Topic.fromJson,
96+
toJson: (item) => item.toJson(),
97+
logger: Logger('HtDataMongodb<Topic>'),
14498
);
145-
topicRepository = _createRepository(
146-
connection,
147-
'topics',
148-
(json) => Topic.fromJson(_convertTimestampsToString(json)),
149-
(topic) => topic.toJson(),
99+
final sourceClient = HtDataMongodb<Source>(
100+
connectionManager: _mongoDbConnectionManager,
101+
modelName: 'sources',
102+
fromJson: Source.fromJson,
103+
toJson: (item) => item.toJson(),
104+
logger: Logger('HtDataMongodb<Source>'),
150105
);
151-
sourceRepository = _createRepository(
152-
connection,
153-
'sources',
154-
(json) => Source.fromJson(_convertTimestampsToString(json)),
155-
(source) => source.toJson()
156-
..['headquarters_country_id'] = source.headquarters.id
157-
..remove('headquarters'),
106+
final countryClient = HtDataMongodb<Country>(
107+
connectionManager: _mongoDbConnectionManager,
108+
modelName: 'countries',
109+
fromJson: Country.fromJson,
110+
toJson: (item) => item.toJson(),
111+
logger: Logger('HtDataMongodb<Country>'),
158112
);
159-
countryRepository = _createRepository(
160-
connection,
161-
'countries',
162-
(json) => Country.fromJson(_convertTimestampsToString(json)),
163-
(country) => country.toJson(),
113+
final userClient = HtDataMongodb<User>(
114+
connectionManager: _mongoDbConnectionManager,
115+
modelName: 'users',
116+
fromJson: User.fromJson,
117+
toJson: (item) => item.toJson(),
118+
logger: Logger('HtDataMongodb<User>'),
164119
);
165-
userRepository = _createRepository(
166-
connection,
167-
'users',
168-
(json) => User.fromJson(_convertTimestampsToString(json)),
169-
(user) {
170-
final json = user.toJson();
171-
// Convert enums to their string names for the database.
172-
json['app_role'] = user.appRole.name;
173-
json['dashboard_role'] = user.dashboardRole.name;
174-
// The `feed_action_status` map must be JSON encoded for the JSONB column.
175-
json['feed_action_status'] = jsonEncode(json['feed_action_status']);
176-
return json;
177-
},
120+
final userAppSettingsClient = HtDataMongodb<UserAppSettings>(
121+
connectionManager: _mongoDbConnectionManager,
122+
modelName: 'user_app_settings',
123+
fromJson: UserAppSettings.fromJson,
124+
toJson: (item) => item.toJson(),
125+
logger: Logger('HtDataMongodb<UserAppSettings>'),
178126
);
179-
userAppSettingsRepository = _createRepository(
180-
connection,
181-
'user_app_settings',
182-
UserAppSettings.fromJson,
183-
(settings) {
184-
final json = settings.toJson();
185-
// These fields are complex objects and must be JSON encoded for the DB.
186-
json['display_settings'] = jsonEncode(json['display_settings']);
187-
json['feed_preferences'] = jsonEncode(json['feed_preferences']);
188-
return json;
189-
},
127+
final userContentPreferencesClient = HtDataMongodb<UserContentPreferences>(
128+
connectionManager: _mongoDbConnectionManager,
129+
modelName: 'user_content_preferences',
130+
fromJson: UserContentPreferences.fromJson,
131+
toJson: (item) => item.toJson(),
132+
logger: Logger('HtDataMongodb<UserContentPreferences>'),
190133
);
191-
userContentPreferencesRepository = _createRepository(
192-
connection,
193-
'user_content_preferences',
194-
UserContentPreferences.fromJson,
195-
(preferences) {
196-
final json = preferences.toJson();
197-
// These fields are lists of complex objects and must be JSON encoded.
198-
json['followed_topics'] = jsonEncode(json['followed_topics']);
199-
json['followed_sources'] = jsonEncode(json['followed_sources']);
200-
json['followed_countries'] = jsonEncode(json['followed_countries']);
201-
json['saved_headlines'] = jsonEncode(json['saved_headlines']);
202-
return json;
203-
},
134+
final remoteConfigClient = HtDataMongodb<RemoteConfig>(
135+
connectionManager: _mongoDbConnectionManager,
136+
modelName: 'remote_configs',
137+
fromJson: RemoteConfig.fromJson,
138+
toJson: (item) => item.toJson(),
139+
logger: Logger('HtDataMongodb<RemoteConfig>'),
204140
);
205-
remoteConfigRepository = _createRepository(
206-
connection,
207-
'remote_config',
208-
(json) => RemoteConfig.fromJson(_convertTimestampsToString(json)),
209-
(config) {
210-
final json = config.toJson();
211-
// All nested config objects must be JSON encoded for JSONB columns.
212-
json['user_preference_limits'] = jsonEncode(
213-
json['user_preference_limits'],
214-
);
215-
json['ad_config'] = jsonEncode(json['ad_config']);
216-
json['account_action_config'] = jsonEncode(
217-
json['account_action_config'],
218-
);
219-
json['app_status'] = jsonEncode(json['app_status']);
220-
return json;
221-
},
141+
142+
// 4. Initialize Repositories
143+
headlineRepository = HtDataRepository(dataClient: headlineClient);
144+
topicRepository = HtDataRepository(dataClient: topicClient);
145+
sourceRepository = HtDataRepository(dataClient: sourceClient);
146+
countryRepository = HtDataRepository(dataClient: countryClient);
147+
userRepository = HtDataRepository(dataClient: userClient);
148+
userAppSettingsRepository =
149+
HtDataRepository(dataClient: userAppSettingsClient);
150+
userContentPreferencesRepository =
151+
HtDataRepository(dataClient: userContentPreferencesClient);
152+
remoteConfigRepository = HtDataRepository(dataClient: remoteConfigClient);
153+
154+
final emailClient = HtEmailInMemoryClient(
155+
logger: Logger('HtEmailInMemoryClient'),
222156
);
157+
emailRepository = HtEmailRepository(emailClient: emailClient);
223158

224-
// 4. Initialize Services.
225-
emailRepository = const HtEmailRepository(
226-
emailClient: HtEmailInMemoryClient(),
159+
// 5. Initialize Services
160+
tokenBlacklistService = InMemoryTokenBlacklistService(
161+
log: Logger('InMemoryTokenBlacklistService'),
227162
);
228-
tokenBlacklistService = InMemoryTokenBlacklistService(log: _log);
229163
authTokenService = JwtAuthTokenService(
230164
userRepository: userRepository,
231165
blacklistService: tokenBlacklistService,
232166
uuidGenerator: const Uuid(),
233-
log: _log,
167+
log: Logger('JwtAuthTokenService'),
234168
);
235169
verificationCodeStorageService = InMemoryVerificationCodeStorageService();
236170
authService = AuthService(
@@ -241,7 +175,7 @@ class AppDependencies {
241175
userAppSettingsRepository: userAppSettingsRepository,
242176
userContentPreferencesRepository: userContentPreferencesRepository,
243177
uuidGenerator: const Uuid(),
244-
log: _log,
178+
log: Logger('AuthService'),
245179
);
246180
dashboardSummaryService = DashboardSummaryService(
247181
headlineRepository: headlineRepository,
@@ -251,40 +185,19 @@ class AppDependencies {
251185
permissionService = const PermissionService();
252186
userPreferenceLimitService = DefaultUserPreferenceLimitService(
253187
remoteConfigRepository: remoteConfigRepository,
254-
log: _log,
188+
log: Logger('DefaultUserPreferenceLimitService'),
255189
);
256-
}
257190

258-
HtDataRepository<T> _createRepository<T>(
259-
Connection connection,
260-
String tableName,
261-
FromJson<T> fromJson,
262-
ToJson<T> toJson,
263-
) {
264-
return HtDataRepository<T>(
265-
dataClient: HtDataPostgresClient<T>(
266-
connection: connection,
267-
tableName: tableName,
268-
fromJson: fromJson,
269-
toJson: toJson,
270-
log: _log,
271-
),
272-
);
191+
_isInitialized = true;
192+
_log.info('Application dependencies initialized successfully.');
273193
}
274194

275-
/// Converts DateTime values in a JSON map to ISO 8601 strings.
276-
///
277-
/// The postgres driver returns DateTime objects for TIMESTAMPTZ columns,
278-
/// but our models' `fromJson` factories expect ISO 8601 strings. This
279-
/// utility function performs the conversion for known timestamp fields.
280-
Map<String, dynamic> _convertTimestampsToString(Map<String, dynamic> json) {
281-
const timestampKeys = {'created_at', 'updated_at'};
282-
final newJson = Map<String, dynamic>.from(json);
283-
for (final key in timestampKeys) {
284-
if (newJson[key] is DateTime) {
285-
newJson[key] = (newJson[key] as DateTime).toIso8601String();
286-
}
287-
}
288-
return newJson;
195+
/// Disposes of resources, such as closing the database connection.
196+
Future<void> dispose() async {
197+
if (!_isInitialized) return;
198+
await _mongoDbConnectionManager.close();
199+
tokenBlacklistService.dispose();
200+
_isInitialized = false;
201+
_log.info('Application dependencies disposed.');
289202
}
290-
}
203+
}

0 commit comments

Comments
 (0)