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' ;
5
2
import 'package:ht_api/src/rbac/permission_service.dart' ;
6
3
import 'package:ht_api/src/services/auth_service.dart' ;
7
4
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';
12
9
import 'package:ht_api/src/services/token_blacklist_service.dart' ;
13
10
import 'package:ht_api/src/services/user_preference_limit_service.dart' ;
14
11
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' ;
17
13
import 'package:ht_data_repository/ht_data_repository.dart' ;
18
14
import 'package:ht_email_inmemory/ht_email_inmemory.dart' ;
19
15
import 'package:ht_email_repository/ht_email_repository.dart' ;
20
16
import 'package:ht_shared/ht_shared.dart' ;
21
17
import 'package:logging/logging.dart' ;
22
- import 'package:postgres/postgres.dart' ;
23
18
import 'package:uuid/uuid.dart' ;
24
19
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}
31
24
class AppDependencies {
25
+ /// Private constructor for the singleton pattern.
32
26
AppDependencies ._();
33
27
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;
36
33
34
+ bool _isInitialized = false ;
37
35
final _log = Logger ('AppDependencies' );
38
- final _completer = Completer <void >();
39
36
40
- // --- Repositories ---
41
- /// A repository for managing [Headline] data.
42
- late final HtDataRepository <Headline > headlineRepository;
37
+ // --- Late-initialized fields for all dependencies ---
43
38
44
- /// A repository for managing [Topic] data.
45
- late final HtDataRepository < Topic > topicRepository ;
39
+ // Database
40
+ late final MongoDbConnectionManager _mongoDbConnectionManager ;
46
41
47
- /// A repository for managing [Source] data.
42
+ // Repositories
43
+ late final HtDataRepository <Headline > headlineRepository;
44
+ late final HtDataRepository <Topic > topicRepository;
48
45
late final HtDataRepository <Source > sourceRepository;
49
-
50
- /// A repository for managing [Country] data.
51
46
late final HtDataRepository <Country > countryRepository;
52
-
53
- /// A repository for managing [User] data.
54
47
late final HtDataRepository <User > userRepository;
55
-
56
- /// A repository for managing [UserAppSettings] data.
57
48
late final HtDataRepository <UserAppSettings > userAppSettingsRepository;
58
-
59
- /// A repository for managing [UserContentPreferences] data.
60
49
late final HtDataRepository <UserContentPreferences >
61
- userContentPreferencesRepository;
62
-
63
- /// A repository for managing the global [RemoteConfig] data.
50
+ userContentPreferencesRepository;
64
51
late final HtDataRepository <RemoteConfig > remoteConfigRepository;
65
-
66
- // --- Services ---
67
- /// A service for sending emails.
68
52
late final HtEmailRepository emailRepository;
69
53
70
- /// A service for managing a blacklist of invalidated authentication tokens.
54
+ // Services
71
55
late final TokenBlacklistService tokenBlacklistService;
72
-
73
- /// A service for generating and validating authentication tokens.
74
56
late final AuthTokenService authTokenService;
75
-
76
- /// A service for storing and validating one-time verification codes.
77
57
late final VerificationCodeStorageService verificationCodeStorageService;
78
-
79
- /// A service that orchestrates authentication logic.
80
58
late final AuthService authService;
81
-
82
- /// A service for calculating and providing a summary for the dashboard.
83
59
late final DashboardSummaryService dashboardSummaryService;
84
-
85
- /// A service for checking user permissions.
86
60
late final PermissionService permissionService;
87
-
88
- /// A service for enforcing limits on user content preferences.
89
61
late final UserPreferenceLimitService userPreferenceLimitService;
90
62
91
63
/// Initializes all application dependencies.
92
64
///
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 ;
100
68
101
69
_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
- }
114
70
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.' ) ;
119
75
120
- // 2. Run Database Seeding.
76
+ // 2. Seed Database
121
77
final seedingService = DatabaseSeedingService (
122
- connection : connection ,
123
- log: _log ,
78
+ db : _mongoDbConnectionManager.db ,
79
+ log: Logger ( 'DatabaseSeedingService' ) ,
124
80
);
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>' ),
144
98
);
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>' ),
150
105
);
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>' ),
158
112
);
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>' ),
164
119
);
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>' ),
178
126
);
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>' ),
190
133
);
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>' ),
204
140
);
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' ),
222
156
);
157
+ emailRepository = HtEmailRepository (emailClient: emailClient);
223
158
224
- // 4 . Initialize Services.
225
- emailRepository = const HtEmailRepository (
226
- emailClient : HtEmailInMemoryClient ( ),
159
+ // 5 . Initialize Services
160
+ tokenBlacklistService = InMemoryTokenBlacklistService (
161
+ log : Logger ( 'InMemoryTokenBlacklistService' ),
227
162
);
228
- tokenBlacklistService = InMemoryTokenBlacklistService (log: _log);
229
163
authTokenService = JwtAuthTokenService (
230
164
userRepository: userRepository,
231
165
blacklistService: tokenBlacklistService,
232
166
uuidGenerator: const Uuid (),
233
- log: _log ,
167
+ log: Logger ( 'JwtAuthTokenService' ) ,
234
168
);
235
169
verificationCodeStorageService = InMemoryVerificationCodeStorageService ();
236
170
authService = AuthService (
@@ -241,7 +175,7 @@ class AppDependencies {
241
175
userAppSettingsRepository: userAppSettingsRepository,
242
176
userContentPreferencesRepository: userContentPreferencesRepository,
243
177
uuidGenerator: const Uuid (),
244
- log: _log ,
178
+ log: Logger ( 'AuthService' ) ,
245
179
);
246
180
dashboardSummaryService = DashboardSummaryService (
247
181
headlineRepository: headlineRepository,
@@ -251,40 +185,19 @@ class AppDependencies {
251
185
permissionService = const PermissionService ();
252
186
userPreferenceLimitService = DefaultUserPreferenceLimitService (
253
187
remoteConfigRepository: remoteConfigRepository,
254
- log: _log ,
188
+ log: Logger ( 'DefaultUserPreferenceLimitService' ) ,
255
189
);
256
- }
257
190
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.' );
273
193
}
274
194
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.' );
289
202
}
290
- }
203
+ }
0 commit comments