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' ;
52import 'package:ht_api/src/rbac/permission_service.dart' ;
63import 'package:ht_api/src/services/auth_service.dart' ;
74import 'package:ht_api/src/services/auth_token_service.dart' ;
@@ -12,225 +9,162 @@ import 'package:ht_api/src/services/jwt_auth_token_service.dart';
129import 'package:ht_api/src/services/token_blacklist_service.dart' ;
1310import 'package:ht_api/src/services/user_preference_limit_service.dart' ;
1411import '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' ;
1713import 'package:ht_data_repository/ht_data_repository.dart' ;
1814import 'package:ht_email_inmemory/ht_email_inmemory.dart' ;
1915import 'package:ht_email_repository/ht_email_repository.dart' ;
2016import 'package:ht_shared/ht_shared.dart' ;
2117import 'package:logging/logging.dart' ;
22- import 'package:postgres/postgres.dart' ;
2318import '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}
3124class 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