-
Notifications
You must be signed in to change notification settings - Fork 0
Fix latest refactor bug #12
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 all commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
3f0805b
fix(api): correct middleware order for Uuid provider
fulleni 3a7b298
fix(api): correct middleware order for Uuid provider
fulleni 4c7031f
feat(api): implement dynamic CORS for local development
fulleni ab9fb6d
docs(api): update README with new dynamic CORS behavior
fulleni 07363bb
style: format
fulleni 7f1c001
fix(api): correct root middleware execution order for Uuid provider
fulleni 8c2e5ce
format: style
fulleni 0e056ef
fix(api): correct v1 middleware composition to preserve context
fulleni 9a02584
feat(api): add dependency container for service location
fulleni 2880ad6
refactor(api): populate dependency container from server
fulleni ae9a641
fix(api): provide all dependencies from root middleware
fulleni 0f2e38a
fix(api): implement custom server entrypoint to fix init race condition
fulleni 60d9563
fix(api): resolve startup race condition with initialization gate
fulleni 9860121
fix(api): implement correct server entrypoint with initialization gate
fulleni ebf04d8
refactor(api): remove server.dart file and related initialization logic
fulleni edb47ad
refactor(api): explicitly type service variables in server config
fulleni 6013ff8
chore: moved server.dart from config folder to directly under the lib…
fulleni 7dbf541
feat(api): add detailed logging to server initialization sequence
fulleni 3d56532
feat(api): add confirmation logging to dependency container
fulleni 1a0a3bf
feat(api): add logging to root middleware for request tracing
fulleni 78e1030
feat(api): add logging to v1 middleware for CORS and auth tracing
fulleni 5712bf9
refactor(api): centralize DI in root middleware
fulleni 2ceed31
refactor(middleware): remove trailing semicolon
fulleni 41d0726
feat(api): create singleton for database connection management
fulleni 8f55afb
feat(api): create singleton for app dependency management
fulleni 881c6b7
refactor(api): centralize DI in root middleware using singletons
fulleni dd394d9
refactor(api): remove obsolete server and dependency container files
fulleni f769493
fix(api): restore root logger configuration in middleware
fulleni a8eff43
feat(api): add diagnostic logging for environment variables
fulleni 2ecb651
feat(api): add dotenv dependency
fulleni cd940ef
feat(api): load .env file directly within app dependencies
fulleni bd1f880
fix(api): correct dotenv initialization and usage
fulleni 87786c2
fix(api): centralize and correct dotenv loading
fulleni c2d52a5
fix(api): centralize and correct dotenv loading in EnvironmentConfig
fulleni 3704dda
fix(api): align categories schema and seeding with data model
fulleni f2af724
refactor(api): remove slug from categories table and seeder
fulleni 5351787
fix(api): align categories table schema with model
fulleni 39e4387
await _connection.execute(
fulleni c5a505a
fix(api): make database seeder resilient to missing optional fields
fulleni 2ed4fbf
feat(api): align sources table schema with data model
fulleni 1ddc3a4
fix(api): align user_app_settings schema with model
fulleni 7369603
fix(api): remove superfluous headquarters object from source seeder p…
fulleni 246f8b0
fix(api): correct source_type column to TEXT in sources schema
fulleni 3dc1109
fix(api): correct seeding order to respect foreign key constraints
fulleni 3b11a62
fix(api): align countries table schema with data model
fulleni 4e9f3ce
fix(api): make headline seeder resilient and align with schema
fulleni 0a777a6
fix(api): align headlines schema and seeder with data model
fulleni 970fced
fix(api): correct syntax and constraints in headlines schema
fulleni b971f54
fix(api): fully align headlines schema and seeder with model
fulleni 86844c0
fix(api): correctly serialize user preferences for JSONB seeding
fulleni b007772
fix(api): correctly serialize roles list for admin user seeding
fulleni fc53d02
fix(api): correctly serialize user roles for database operations
fulleni 5ad4d72
fix(api): handle DateTime deserialization from database for User model
fulleni 43454b4
fix(api): align user_app_settings schema with data model
fulleni 757baa8
fix(api): correctly serialize and deserialize user content preferences
fulleni 292d5c8
fix(api): provide model registry to root middleware
fulleni b81371a
fix(api): correct data serialization for all repositories
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,302 @@ | ||
import 'dart:async'; | ||
import 'dart:convert'; | ||
|
||
import 'package:ht_api/src/config/database_connection.dart'; | ||
import 'package:ht_api/src/rbac/permission_service.dart'; | ||
import 'package:ht_api/src/services/auth_service.dart'; | ||
import 'package:ht_api/src/services/auth_token_service.dart'; | ||
import 'package:ht_api/src/services/dashboard_summary_service.dart'; | ||
import 'package:ht_api/src/services/database_seeding_service.dart'; | ||
import 'package:ht_api/src/services/default_user_preference_limit_service.dart'; | ||
import 'package:ht_api/src/services/jwt_auth_token_service.dart'; | ||
import 'package:ht_api/src/services/token_blacklist_service.dart'; | ||
import 'package:ht_api/src/services/user_preference_limit_service.dart'; | ||
import 'package:ht_api/src/services/verification_code_storage_service.dart'; | ||
import 'package:ht_data_client/ht_data_client.dart'; | ||
import 'package:ht_data_postgres/ht_data_postgres.dart'; | ||
import 'package:ht_data_repository/ht_data_repository.dart'; | ||
import 'package:ht_email_inmemory/ht_email_inmemory.dart'; | ||
import 'package:ht_email_repository/ht_email_repository.dart'; | ||
import 'package:ht_shared/ht_shared.dart'; | ||
import 'package:logging/logging.dart'; | ||
import 'package:postgres/postgres.dart'; | ||
import 'package:uuid/uuid.dart'; | ||
|
||
/// A singleton class to manage all application dependencies. | ||
/// | ||
/// This class follows a lazy initialization pattern. Dependencies are created | ||
/// only when the `init()` method is first called, typically triggered by the | ||
/// first incoming request. A `Completer` ensures that subsequent requests | ||
/// await the completion of the initial setup. | ||
class AppDependencies { | ||
AppDependencies._(); | ||
|
||
/// The single, global instance of the [AppDependencies]. | ||
static final instance = AppDependencies._(); | ||
|
||
final _log = Logger('AppDependencies'); | ||
final _completer = Completer<void>(); | ||
|
||
// --- Repositories --- | ||
late final HtDataRepository<Headline> headlineRepository; | ||
late final HtDataRepository<Category> categoryRepository; | ||
late final HtDataRepository<Source> sourceRepository; | ||
late final HtDataRepository<Country> countryRepository; | ||
late final HtDataRepository<User> userRepository; | ||
late final HtDataRepository<UserAppSettings> userAppSettingsRepository; | ||
late final HtDataRepository<UserContentPreferences> | ||
userContentPreferencesRepository; | ||
late final HtDataRepository<AppConfig> appConfigRepository; | ||
|
||
// --- Services --- | ||
late final HtEmailRepository emailRepository; | ||
late final TokenBlacklistService tokenBlacklistService; | ||
late final AuthTokenService authTokenService; | ||
late final VerificationCodeStorageService verificationCodeStorageService; | ||
late final AuthService authService; | ||
late final DashboardSummaryService dashboardSummaryService; | ||
late final PermissionService permissionService; | ||
late final UserPreferenceLimitService userPreferenceLimitService; | ||
|
||
/// Initializes all application dependencies. | ||
/// | ||
/// This method is idempotent. It performs the full initialization only on | ||
/// the first call. Subsequent calls will await the result of the first one. | ||
Future<void> init() { | ||
if (_completer.isCompleted) { | ||
_log.fine('Dependencies already initializing/initialized.'); | ||
return _completer.future; | ||
} | ||
|
||
_log.info('Initializing application dependencies...'); | ||
_init() | ||
.then((_) { | ||
_log.info('Application dependencies initialized successfully.'); | ||
_completer.complete(); | ||
}) | ||
.catchError((Object e, StackTrace s) { | ||
_log.severe('Failed to initialize application dependencies.', e, s); | ||
_completer.completeError(e, s); | ||
}); | ||
|
||
return _completer.future; | ||
} | ||
|
||
Future<void> _init() async { | ||
// 1. Establish Database Connection. | ||
await DatabaseConnectionManager.instance.init(); | ||
final connection = await DatabaseConnectionManager.instance.connection; | ||
|
||
// 2. Run Database Seeding. | ||
final seedingService = DatabaseSeedingService( | ||
connection: connection, | ||
log: _log, | ||
); | ||
await seedingService.createTables(); | ||
await seedingService.seedGlobalFixtureData(); | ||
await seedingService.seedInitialAdminAndConfig(); | ||
|
||
// 3. Initialize Repositories. | ||
headlineRepository = _createRepository( | ||
connection, | ||
'headlines', | ||
(json) { | ||
if (json['created_at'] is DateTime) { | ||
json['created_at'] = | ||
(json['created_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['updated_at'] is DateTime) { | ||
json['updated_at'] = | ||
(json['updated_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['published_at'] is DateTime) { | ||
json['published_at'] = | ||
(json['published_at'] as DateTime).toIso8601String(); | ||
} | ||
return Headline.fromJson(json); | ||
}, | ||
fulleni marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(h) => h.toJson(), // toJson already handles DateTime correctly | ||
); | ||
categoryRepository = _createRepository( | ||
connection, | ||
'categories', | ||
(json) { | ||
if (json['created_at'] is DateTime) { | ||
json['created_at'] = | ||
(json['created_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['updated_at'] is DateTime) { | ||
json['updated_at'] = | ||
(json['updated_at'] as DateTime).toIso8601String(); | ||
} | ||
return Category.fromJson(json); | ||
}, | ||
(c) => c.toJson(), | ||
); | ||
sourceRepository = _createRepository( | ||
connection, | ||
'sources', | ||
(json) { | ||
if (json['created_at'] is DateTime) { | ||
json['created_at'] = | ||
(json['created_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['updated_at'] is DateTime) { | ||
json['updated_at'] = | ||
(json['updated_at'] as DateTime).toIso8601String(); | ||
} | ||
return Source.fromJson(json); | ||
}, | ||
(s) => s.toJson(), | ||
); | ||
countryRepository = _createRepository( | ||
connection, | ||
'countries', | ||
(json) { | ||
if (json['created_at'] is DateTime) { | ||
json['created_at'] = | ||
(json['created_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['updated_at'] is DateTime) { | ||
json['updated_at'] = | ||
(json['updated_at'] as DateTime).toIso8601String(); | ||
} | ||
return Country.fromJson(json); | ||
}, | ||
(c) => c.toJson(), | ||
); | ||
userRepository = _createRepository( | ||
connection, | ||
'users', | ||
(json) { | ||
// The postgres driver returns DateTime objects, but the model's | ||
// fromJson expects ISO 8601 strings. We must convert them first. | ||
if (json['created_at'] is DateTime) { | ||
json['created_at'] = (json['created_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['last_engagement_shown_at'] is DateTime) { | ||
json['last_engagement_shown_at'] = | ||
(json['last_engagement_shown_at'] as DateTime).toIso8601String(); | ||
} | ||
return User.fromJson(json); | ||
}, | ||
(user) { | ||
// The `roles` field is a List<String>, but the database expects a | ||
// JSONB array. We must explicitly encode it. | ||
final json = user.toJson(); | ||
json['roles'] = jsonEncode(json['roles']); | ||
return json; | ||
}, | ||
); | ||
userAppSettingsRepository = _createRepository( | ||
connection, | ||
'user_app_settings', | ||
(json) { | ||
// The DB has created_at/updated_at, but the model doesn't. | ||
// Remove them before deserialization to avoid CheckedFromJsonException. | ||
json.remove('created_at'); | ||
json.remove('updated_at'); | ||
return UserAppSettings.fromJson(json); | ||
}, | ||
(settings) { | ||
final json = settings.toJson(); | ||
// These fields are complex objects and must be JSON encoded for the DB. | ||
json['display_settings'] = jsonEncode(json['display_settings']); | ||
json['feed_preferences'] = jsonEncode(json['feed_preferences']); | ||
json['engagement_shown_counts'] = | ||
jsonEncode(json['engagement_shown_counts']); | ||
json['engagement_last_shown_timestamps'] = | ||
jsonEncode(json['engagement_last_shown_timestamps']); | ||
return json; | ||
}, | ||
); | ||
userContentPreferencesRepository = _createRepository( | ||
connection, | ||
'user_content_preferences', | ||
(json) { | ||
// The postgres driver returns DateTime objects, but the model's | ||
// fromJson expects ISO 8601 strings. We must convert them first. | ||
if (json['created_at'] is DateTime) { | ||
json['created_at'] = | ||
(json['created_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['updated_at'] is DateTime) { | ||
json['updated_at'] = | ||
(json['updated_at'] as DateTime).toIso8601String(); | ||
} | ||
return UserContentPreferences.fromJson(json); | ||
}, | ||
(preferences) { | ||
final json = preferences.toJson(); | ||
json['followed_categories'] = jsonEncode(json['followed_categories']); | ||
json['followed_sources'] = jsonEncode(json['followed_sources']); | ||
json['followed_countries'] = jsonEncode(json['followed_countries']); | ||
json['saved_headlines'] = jsonEncode(json['saved_headlines']); | ||
return json; | ||
}, | ||
); | ||
appConfigRepository = _createRepository( | ||
connection, | ||
'app_config', | ||
(json) { | ||
if (json['created_at'] is DateTime) { | ||
json['created_at'] = | ||
(json['created_at'] as DateTime).toIso8601String(); | ||
} | ||
if (json['updated_at'] is DateTime) { | ||
json['updated_at'] = | ||
(json['updated_at'] as DateTime).toIso8601String(); | ||
} | ||
return AppConfig.fromJson(json); | ||
}, | ||
(c) => c.toJson(), | ||
); | ||
|
||
// 4. Initialize Services. | ||
emailRepository = const HtEmailRepository( | ||
emailClient: HtEmailInMemoryClient(), | ||
); | ||
tokenBlacklistService = InMemoryTokenBlacklistService(); | ||
authTokenService = JwtAuthTokenService( | ||
userRepository: userRepository, | ||
blacklistService: tokenBlacklistService, | ||
uuidGenerator: const Uuid(), | ||
); | ||
verificationCodeStorageService = InMemoryVerificationCodeStorageService(); | ||
authService = AuthService( | ||
userRepository: userRepository, | ||
authTokenService: authTokenService, | ||
verificationCodeStorageService: verificationCodeStorageService, | ||
emailRepository: emailRepository, | ||
userAppSettingsRepository: userAppSettingsRepository, | ||
userContentPreferencesRepository: userContentPreferencesRepository, | ||
uuidGenerator: const Uuid(), | ||
); | ||
dashboardSummaryService = DashboardSummaryService( | ||
headlineRepository: headlineRepository, | ||
categoryRepository: categoryRepository, | ||
sourceRepository: sourceRepository, | ||
); | ||
permissionService = const PermissionService(); | ||
userPreferenceLimitService = DefaultUserPreferenceLimitService( | ||
appConfigRepository: appConfigRepository, | ||
); | ||
} | ||
|
||
HtDataRepository<T> _createRepository<T>( | ||
Connection connection, | ||
String tableName, | ||
FromJson<T> fromJson, | ||
ToJson<T> toJson, | ||
) { | ||
return HtDataRepository<T>( | ||
dataClient: HtDataPostgresClient<T>( | ||
connection: connection, | ||
tableName: tableName, | ||
fromJson: fromJson, | ||
toJson: toJson, | ||
log: _log, | ||
), | ||
); | ||
} | ||
} |
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,67 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:ht_api/src/config/environment_config.dart'; | ||
import 'package:logging/logging.dart'; | ||
import 'package:postgres/postgres.dart'; | ||
|
||
/// A singleton class to manage a single, shared PostgreSQL database connection. | ||
/// | ||
/// This pattern ensures that the application establishes a connection to the | ||
/// database only once and reuses it for all subsequent operations, which is | ||
/// crucial for performance and resource management. | ||
class DatabaseConnectionManager { | ||
// Private constructor for the singleton pattern. | ||
DatabaseConnectionManager._(); | ||
|
||
/// The single, global instance of the [DatabaseConnectionManager]. | ||
static final instance = DatabaseConnectionManager._(); | ||
|
||
final _log = Logger('DatabaseConnectionManager'); | ||
|
||
// A completer to signal when the database connection is established. | ||
final _completer = Completer<Connection>(); | ||
|
||
/// Returns a future that completes with the established database connection. | ||
/// | ||
/// If the connection has not been initialized yet, it calls `init()` to | ||
/// establish it. Subsequent calls will return the same connection future. | ||
Future<Connection> get connection => _completer.future; | ||
|
||
/// Initializes the database connection. | ||
/// | ||
/// This method is idempotent. It parses the database URL from the | ||
/// environment, opens a connection to the PostgreSQL server, and completes | ||
/// the `_completer` with the connection. It only performs the connection | ||
/// logic on the very first call. | ||
Future<void> init() async { | ||
if (_completer.isCompleted) { | ||
_log.fine('Database connection already initializing/initialized.'); | ||
return; | ||
} | ||
|
||
_log.info('Initializing database connection...'); | ||
final dbUri = Uri.parse(EnvironmentConfig.databaseUrl); | ||
String? username; | ||
String? password; | ||
if (dbUri.userInfo.isNotEmpty) { | ||
final parts = dbUri.userInfo.split(':'); | ||
username = Uri.decodeComponent(parts.first); | ||
if (parts.length > 1) { | ||
password = Uri.decodeComponent(parts.last); | ||
} | ||
} | ||
|
||
final connection = await Connection.open( | ||
Endpoint( | ||
host: dbUri.host, | ||
port: dbUri.port, | ||
database: dbUri.path.substring(1), | ||
username: username, | ||
password: password, | ||
), | ||
settings: const ConnectionSettings(sslMode: SslMode.require), | ||
); | ||
_log.info('Database connection established successfully.'); | ||
_completer.complete(connection); | ||
} | ||
} |
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.