1
+ import 'dart:async' ;
2
+ import 'dart:io' ;
3
+
4
+ import 'package:dart_frog/dart_frog.dart' ;
5
+ import 'package:ht_api/src/config/dependency_container.dart' ;
6
+ import 'package:ht_api/src/config/environment_config.dart' ;
7
+ import 'package:ht_api/src/rbac/permission_service.dart' ;
8
+ import 'package:ht_api/src/services/auth_service.dart' ;
9
+ import 'package:ht_api/src/services/auth_token_service.dart' ;
10
+ import 'package:ht_api/src/services/dashboard_summary_service.dart' ;
11
+ import 'package:ht_api/src/services/database_seeding_service.dart' ;
12
+ import 'package:ht_api/src/services/default_user_preference_limit_service.dart' ;
13
+ import 'package:ht_api/src/services/jwt_auth_token_service.dart' ;
14
+ import 'package:ht_api/src/services/token_blacklist_service.dart' ;
15
+ import 'package:ht_api/src/services/user_preference_limit_service.dart' ;
16
+ import 'package:ht_api/src/services/verification_code_storage_service.dart' ;
17
+ import 'package:ht_data_client/ht_data_client.dart' ;
18
+ import 'package:ht_data_postgres/ht_data_postgres.dart' ;
19
+ import 'package:ht_data_repository/ht_data_repository.dart' ;
20
+ import 'package:ht_email_inmemory/ht_email_inmemory.dart' ;
21
+ import 'package:ht_email_repository/ht_email_repository.dart' ;
22
+ import 'package:ht_shared/ht_shared.dart' ;
23
+ import 'package:logging/logging.dart' ;
24
+ import 'package:postgres/postgres.dart' ;
25
+ import 'package:uuid/uuid.dart' ;
26
+
27
+ /// Global logger instance.
28
+ final _log = Logger ('ht_api' );
29
+
30
+ /// Global PostgreSQL connection instance.
31
+ late final Connection _connection;
32
+
33
+ /// Creates a data repository for a given type [T] .
34
+ HtDataRepository <T > _createRepository <T >({
35
+ required String tableName,
36
+ required FromJson <T > fromJson,
37
+ required ToJson <T > toJson,
38
+ }) {
39
+ return HtDataRepository <T >(
40
+ dataClient: HtDataPostgresClient <T >(
41
+ connection: _connection,
42
+ tableName: tableName,
43
+ fromJson: fromJson,
44
+ toJson: toJson,
45
+ log: _log,
46
+ ),
47
+ );
48
+ }
49
+
50
+ /// The main entry point for the server, used by `dart_frog dev` .
51
+ ///
52
+ /// This function is responsible for the entire server startup sequence:
53
+ /// 1. **Gating Requests:** It immediately sets up a "gate" using a `Completer`
54
+ /// to hold all incoming requests until initialization is complete.
55
+ /// 2. **Async Initialization:** It performs all necessary asynchronous setup,
56
+ /// including logging, database connection, and data seeding.
57
+ /// 3. **Dependency Injection:** It initializes all repositories and services
58
+ /// and populates the `DependencyContainer`.
59
+ /// 4. **Server Start:** It starts the HTTP server with the gated handler.
60
+ /// 5. **Opening the Gate:** Once the server is listening, it completes the
61
+ /// `Completer`, allowing the gated requests to be processed.
62
+ /// 6. **Graceful Shutdown:** It sets up a listener for `SIGINT` to close
63
+ /// resources gracefully.
64
+ Future <HttpServer > run (Handler handler, InternetAddress ip, int port) async {
65
+ final initCompleter = Completer <void >();
66
+
67
+ // This middleware wraps the main handler. It awaits the completer's future,
68
+ // effectively pausing the request until `initCompleter.complete()` is called.
69
+ final gatedHandler = handler.use (
70
+ (innerHandler) {
71
+ return (context) async {
72
+ await initCompleter.future;
73
+ return innerHandler (context);
74
+ };
75
+ },
76
+ );
77
+
78
+ // 1. Setup Logger
79
+ Logger .root.level = Level .ALL ;
80
+ Logger .root.onRecord.listen ((record) {
81
+ // ignore: avoid_print
82
+ print (
83
+ '${record .level .name }: ${record .time }: '
84
+ '${record .loggerName }: ${record .message }' ,
85
+ );
86
+ });
87
+
88
+ // 2. Establish Database Connection
89
+ _log.info ('Connecting to PostgreSQL database...' );
90
+ final dbUri = Uri .parse (EnvironmentConfig .databaseUrl);
91
+ String ? username;
92
+ String ? password;
93
+ if (dbUri.userInfo.isNotEmpty) {
94
+ final parts = dbUri.userInfo.split (':' );
95
+ username = Uri .decodeComponent (parts.first);
96
+ if (parts.length > 1 ) {
97
+ password = Uri .decodeComponent (parts.last);
98
+ }
99
+ }
100
+
101
+ _connection = await Connection .open (
102
+ Endpoint (
103
+ host: dbUri.host,
104
+ port: dbUri.port,
105
+ database: dbUri.path.substring (1 ), // Remove leading '/'
106
+ username: username,
107
+ password: password,
108
+ ),
109
+ settings: const ConnectionSettings (sslMode: SslMode .require),
110
+ );
111
+ _log.info ('PostgreSQL database connection established.' );
112
+
113
+ // 3. Initialize and run database seeding
114
+ final seedingService = DatabaseSeedingService (
115
+ connection: _connection,
116
+ log: _log,
117
+ );
118
+ await seedingService.createTables ();
119
+ await seedingService.seedGlobalFixtureData ();
120
+ await seedingService.seedInitialAdminAndConfig ();
121
+
122
+ // 4. Initialize Repositories
123
+ final headlineRepository = _createRepository <Headline >(
124
+ tableName: 'headlines' ,
125
+ fromJson: Headline .fromJson,
126
+ toJson: (h) => h.toJson (),
127
+ );
128
+ final categoryRepository = _createRepository <Category >(
129
+ tableName: 'categories' ,
130
+ fromJson: Category .fromJson,
131
+ toJson: (c) => c.toJson (),
132
+ );
133
+ final sourceRepository = _createRepository <Source >(
134
+ tableName: 'sources' ,
135
+ fromJson: Source .fromJson,
136
+ toJson: (s) => s.toJson (),
137
+ );
138
+ final countryRepository = _createRepository <Country >(
139
+ tableName: 'countries' ,
140
+ fromJson: Country .fromJson,
141
+ toJson: (c) => c.toJson (),
142
+ );
143
+ final userRepository = _createRepository <User >(
144
+ tableName: 'users' ,
145
+ fromJson: User .fromJson,
146
+ toJson: (u) => u.toJson (),
147
+ );
148
+ final userAppSettingsRepository = _createRepository <UserAppSettings >(
149
+ tableName: 'user_app_settings' ,
150
+ fromJson: UserAppSettings .fromJson,
151
+ toJson: (s) => s.toJson (),
152
+ );
153
+ final userContentPreferencesRepository =
154
+ _createRepository <UserContentPreferences >(
155
+ tableName: 'user_content_preferences' ,
156
+ fromJson: UserContentPreferences .fromJson,
157
+ toJson: (p) => p.toJson (),
158
+ );
159
+ final appConfigRepository = _createRepository <AppConfig >(
160
+ tableName: 'app_config' ,
161
+ fromJson: AppConfig .fromJson,
162
+ toJson: (c) => c.toJson (),
163
+ );
164
+
165
+ // 5. Initialize Services
166
+ const emailRepository = HtEmailRepository (
167
+ emailClient: HtEmailInMemoryClient (),
168
+ );
169
+ final tokenBlacklistService = InMemoryTokenBlacklistService ();
170
+ final authTokenService = JwtAuthTokenService (
171
+ userRepository: userRepository,
172
+ blacklistService: tokenBlacklistService,
173
+ uuidGenerator: const Uuid (),
174
+ );
175
+ final verificationCodeStorageService =
176
+ InMemoryVerificationCodeStorageService ();
177
+ final authService = AuthService (
178
+ userRepository: userRepository,
179
+ authTokenService: authTokenService,
180
+ verificationCodeStorageService: verificationCodeStorageService,
181
+ emailRepository: emailRepository,
182
+ userAppSettingsRepository: userAppSettingsRepository,
183
+ userContentPreferencesRepository: userContentPreferencesRepository,
184
+ uuidGenerator: const Uuid (),
185
+ );
186
+ final dashboardSummaryService = DashboardSummaryService (
187
+ headlineRepository: headlineRepository,
188
+ categoryRepository: categoryRepository,
189
+ sourceRepository: sourceRepository,
190
+ );
191
+ const permissionService = PermissionService ();
192
+ final userPreferenceLimitService = DefaultUserPreferenceLimitService (
193
+ appConfigRepository: appConfigRepository,
194
+ );
195
+
196
+ // 6. Populate the DependencyContainer
197
+ DependencyContainer .instance.init (
198
+ headlineRepository: headlineRepository,
199
+ categoryRepository: categoryRepository,
200
+ sourceRepository: sourceRepository,
201
+ countryRepository: countryRepository,
202
+ userRepository: userRepository,
203
+ userAppSettingsRepository: userAppSettingsRepository,
204
+ userContentPreferencesRepository: userContentPreferencesRepository,
205
+ appConfigRepository: appConfigRepository,
206
+ emailRepository: emailRepository,
207
+ tokenBlacklistService: tokenBlacklistService,
208
+ authTokenService: authTokenService,
209
+ verificationCodeStorageService: verificationCodeStorageService,
210
+ authService: authService,
211
+ dashboardSummaryService: dashboardSummaryService,
212
+ permissionService: permissionService,
213
+ userPreferenceLimitService: userPreferenceLimitService,
214
+ );
215
+
216
+ // 7. Start the server with the gated handler
217
+ final server = await serve (gatedHandler, ip, port);
218
+ _log.info ('Server listening on port ${server .port }' );
219
+
220
+ // 8. Open the gate now that the server is ready.
221
+ initCompleter.complete ();
222
+
223
+ // 9. Handle graceful shutdown
224
+ ProcessSignal .sigint.watch ().listen ((_) async {
225
+ _log.info ('Received SIGINT. Shutting down...' );
226
+ await _connection.close ();
227
+ _log.info ('Database connection closed.' );
228
+ await server.close (force: true );
229
+ _log.info ('Server shut down.' );
230
+ exit (0 );
231
+ });
232
+
233
+ return server;
234
+ }
0 commit comments