Skip to content

Commit eaee368

Browse files
authored
Merge pull request #19 from headlines-toolkit/refactor_improve_router_response_handlers
Refactor improve router response handlers
2 parents d532bc4 + e37b24c commit eaee368

15 files changed

+186
-246
lines changed

lib/src/config/app_dependencies.dart

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class AppDependencies {
4949
late final HtDataRepository<User> userRepository;
5050
late final HtDataRepository<UserAppSettings> userAppSettingsRepository;
5151
late final HtDataRepository<UserContentPreferences>
52-
userContentPreferencesRepository;
52+
userContentPreferencesRepository;
5353
late final HtDataRepository<RemoteConfig> remoteConfigRepository;
5454
late final HtEmailRepository emailRepository;
5555

@@ -134,12 +134,12 @@ class AppDependencies {
134134
);
135135
final userContentPreferencesClient =
136136
HtDataMongodb<UserContentPreferences>(
137-
connectionManager: _mongoDbConnectionManager,
138-
modelName: 'user_content_preferences',
139-
fromJson: UserContentPreferences.fromJson,
140-
toJson: (item) => item.toJson(),
141-
logger: Logger('HtDataMongodb<UserContentPreferences>'),
142-
);
137+
connectionManager: _mongoDbConnectionManager,
138+
modelName: 'user_content_preferences',
139+
fromJson: UserContentPreferences.fromJson,
140+
toJson: (item) => item.toJson(),
141+
logger: Logger('HtDataMongodb<UserContentPreferences>'),
142+
);
143143
final remoteConfigClient = HtDataMongodb<RemoteConfig>(
144144
connectionManager: _mongoDbConnectionManager,
145145
modelName: 'remote_configs',
@@ -154,12 +154,13 @@ class AppDependencies {
154154
sourceRepository = HtDataRepository(dataClient: sourceClient);
155155
countryRepository = HtDataRepository(dataClient: countryClient);
156156
userRepository = HtDataRepository(dataClient: userClient);
157-
userAppSettingsRepository =
158-
HtDataRepository(dataClient: userAppSettingsClient);
159-
userContentPreferencesRepository =
160-
HtDataRepository(dataClient: userContentPreferencesClient);
161-
remoteConfigRepository =
162-
HtDataRepository(dataClient: remoteConfigClient);
157+
userAppSettingsRepository = HtDataRepository(
158+
dataClient: userAppSettingsClient,
159+
);
160+
userContentPreferencesRepository = HtDataRepository(
161+
dataClient: userContentPreferencesClient,
162+
);
163+
remoteConfigRepository = HtDataRepository(dataClient: remoteConfigClient);
163164

164165
final emailClient = HtEmailInMemoryClient(
165166
logger: Logger('HtEmailInMemoryClient'),
@@ -176,8 +177,7 @@ class AppDependencies {
176177
uuidGenerator: const Uuid(),
177178
log: Logger('JwtAuthTokenService'),
178179
);
179-
verificationCodeStorageService =
180-
InMemoryVerificationCodeStorageService();
180+
verificationCodeStorageService = InMemoryVerificationCodeStorageService();
181181
permissionService = const PermissionService();
182182
authService = AuthService(
183183
userRepository: userRepository,
@@ -219,4 +219,4 @@ class AppDependencies {
219219
_isInitialized = false;
220220
_log.info('Application dependencies disposed.');
221221
}
222-
}
222+
}

lib/src/config/environment_config.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,19 @@ abstract final class EnvironmentConfig {
4343
env.load([envPath]); // Load variables from the found .env file
4444
return env; // Return immediately upon finding
4545
} else {
46-
_log.warning('pubspec.yaml found, but no .env in the same directory.');
46+
_log.warning(
47+
'pubspec.yaml found, but no .env in the same directory.',
48+
);
4749
break; // Stop searching since pubspec.yaml should contain .env
4850
}
4951
}
5052
currentDir = currentDir.parent; // Move to the parent directory
5153
_log.finer('Moving up to parent directory: ${currentDir.path}');
5254
}
5355
// If loop completes without returning, .env was not found
54-
_log.warning('.env not found by searching. Falling back to default load().');
56+
_log.warning(
57+
'.env not found by searching. Falling back to default load().',
58+
);
5559
env.load(); // Fallback to default load
5660
return env; // Return even if fallback
5761
}

lib/src/helpers/response_helper.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog/dart_frog.dart';
4+
import 'package:ht_api/src/models/request_id.dart';
5+
import 'package:ht_shared/ht_shared.dart';
6+
7+
/// A utility class to simplify the creation of standardized API responses.
8+
abstract final class ResponseHelper {
9+
/// Creates a standardized success JSON response.
10+
///
11+
/// This helper encapsulates the boilerplate of creating metadata, wrapping the
12+
/// payload in a [SuccessApiResponse], and serializing it to JSON.
13+
///
14+
/// - [context]: The request context, used to read the `RequestId`.
15+
/// - [data]: The payload to be included in the response.
16+
/// - [toJsonT]: A function that knows how to serialize the [data] payload.
17+
/// This is necessary because of Dart's generics. For a simple object, this
18+
/// would be `(data) => data.toJson()`. For a paginated list, it would be
19+
/// `(paginated) => paginated.toJson((item) => item.toJson())`.
20+
/// - [statusCode]: The HTTP status code for the response. Defaults to 200 OK.
21+
static Response success<T>({
22+
required RequestContext context,
23+
required T data,
24+
required Map<String, dynamic> Function(T data) toJsonT,
25+
int statusCode = HttpStatus.ok,
26+
}) {
27+
final metadata = ResponseMetadata(
28+
requestId: context.read<RequestId>().id,
29+
timestamp: DateTime.now().toUtc(),
30+
);
31+
32+
final responsePayload = SuccessApiResponse<T>(
33+
data: data,
34+
metadata: metadata,
35+
);
36+
37+
return Response.json(
38+
statusCode: statusCode,
39+
body: responsePayload.toJson(toJsonT),
40+
);
41+
}
42+
}

lib/src/middlewares/error_handler.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ Middleware errorHandler() {
2929
} on CheckedFromJsonException catch (e, stackTrace) {
3030
// Handle json_serializable validation errors. These are client errors.
3131
final field = e.key ?? 'unknown';
32-
final message = 'Invalid request body: Field "$field" has an '
32+
final message =
33+
'Invalid request body: Field "$field" has an '
3334
'invalid value or is missing. ${e.message}';
3435
print('CheckedFromJsonException Caught: $e\n$stackTrace');
3536
return _jsonErrorResponse(
@@ -50,7 +51,9 @@ Middleware errorHandler() {
5051
print('Unhandled Exception Caught: $e\n$stackTrace');
5152
return _jsonErrorResponse(
5253
statusCode: HttpStatus.internalServerError, // 500
53-
exception: const UnknownException('An unexpected internal server error occurred.'),
54+
exception: const UnknownException(
55+
'An unexpected internal server error occurred.',
56+
),
5457
context: context,
5558
);
5659
}
@@ -137,7 +140,9 @@ Response _jsonErrorResponse({
137140

138141
return Response.json(
139142
statusCode: statusCode,
140-
body: {'error': {'code': errorCode, 'message': exception.message}},
143+
body: {
144+
'error': {'code': errorCode, 'message': exception.message},
145+
},
141146
headers: headers,
142147
);
143148
}

lib/src/models/request_id.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/// {@template request_id}
2+
/// A wrapper class holding a unique identifier (UUID v4) generated for each
3+
/// incoming HTTP request.
4+
///
5+
/// **Purpose:**
6+
/// The primary role of this ID is **traceability for logging and debugging**.
7+
/// It allows developers to follow the entire lifecycle of a *single request*
8+
/// through various middleware, route handlers, repository calls, and potential
9+
/// external service interactions by searching logs for this specific ID.
10+
/// If an error occurs during a request, this ID provides a way to isolate all
11+
/// related log entries for that specific transaction, simplifying debugging.
12+
///
13+
/// **Scope:**
14+
/// - The ID is **transient** for the request itself; it exists only during the
15+
/// request-response cycle.
16+
/// - It is **not persisted** in the main application database alongside models
17+
/// like Headlines or Categories.
18+
/// - Its value lies in being included in **persistent logs**.
19+
///
20+
/// **Distinction from other IDs:**
21+
/// - **User ID:** Identifies the authenticated user making the request. Often
22+
/// logged alongside the `request_id` for user-specific debugging.
23+
/// - **Session ID:** Tracks a user's session across multiple requests.
24+
/// - **Correlation ID:** Often generated by the *client* and passed in headers
25+
/// to link related requests initiated by the client for a larger workflow.
26+
///
27+
/// **Implementation:**
28+
/// This class ensures type safety when providing and reading the request ID
29+
/// from the Dart Frog context using `context.provide<RequestId>` and
30+
/// `context.read<RequestId>()`. This prevents potential ambiguity if other raw
31+
/// strings were provided into the context.
32+
/// {@endtemplate}
33+
class RequestId {
34+
/// {@macro request_id}
35+
const RequestId(this.id);
36+
37+
/// The unique identifier string (UUID v4).
38+
final String id;
39+
}

lib/src/services/auth_service.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ class AuthService {
2323
required HtEmailRepository emailRepository,
2424
required HtDataRepository<UserAppSettings> userAppSettingsRepository,
2525
required HtDataRepository<UserContentPreferences>
26-
userContentPreferencesRepository,
26+
userContentPreferencesRepository,
2727
required PermissionService permissionService,
2828
required Uuid uuidGenerator,
2929
required Logger log,
3030
}) : _userRepository = userRepository,
3131
_authTokenService = authTokenService,
3232
_verificationCodeStorageService = verificationCodeStorageService,
33-
_permissionService = permissionService,
33+
_permissionService = permissionService,
3434
_emailRepository = emailRepository,
3535
_userAppSettingsRepository = userAppSettingsRepository,
3636
_userContentPreferencesRepository = userContentPreferencesRepository,
@@ -43,7 +43,7 @@ class AuthService {
4343
final HtEmailRepository _emailRepository;
4444
final HtDataRepository<UserAppSettings> _userAppSettingsRepository;
4545
final HtDataRepository<UserContentPreferences>
46-
_userContentPreferencesRepository;
46+
_userContentPreferencesRepository;
4747
final PermissionService _permissionService;
4848
final Logger _log;
4949
final Uuid _uuid;
@@ -83,7 +83,10 @@ class AuthService {
8383
throw const UnauthorizedException(
8484
'This email address is not registered for dashboard access.',
8585
);
86-
} else if (!_permissionService.hasPermission(user, Permissions.dashboardLogin)) {
86+
} else if (!_permissionService.hasPermission(
87+
user,
88+
Permissions.dashboardLogin,
89+
)) {
8790
_log.warning(
8891
'Dashboard login failed: User ${user.id} lacks required permission (${Permissions.dashboardLogin}).',
8992
);
@@ -273,7 +276,9 @@ class AuthService {
273276
rethrow;
274277
} catch (e, s) {
275278
_log.severe(
276-
'Unexpected error during user lookup/creation for $email: $e', e, s,
279+
'Unexpected error during user lookup/creation for $email: $e',
280+
e,
281+
s,
277282
);
278283
throw const OperationFailedException('Failed to process user account.');
279284
}
@@ -652,9 +657,7 @@ class AuthService {
652657
/// Re-throws any [HtHttpException] from the repository.
653658
Future<User?> _findUserByEmail(String email) async {
654659
try {
655-
final response = await _userRepository.readAll(
656-
filter: {'email': email},
657-
);
660+
final response = await _userRepository.readAll(filter: {'email': email});
658661
if (response.items.isNotEmpty) {
659662
return response.items.first;
660663
}

lib/src/services/database_seeding_service.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class DatabaseSeedingService {
9292
// Filter by the specific, deterministic _id.
9393
'filter': {'_id': objectId},
9494
// Set the fields of the document.
95-
'update': {'\$set': document},
95+
'update': {r'$set': document},
9696
'upsert': true,
9797
},
9898
});

routes/_middleware.dart

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:dart_frog/dart_frog.dart';
22
import 'package:ht_api/src/config/app_dependencies.dart';
33
import 'package:ht_api/src/middlewares/error_handler.dart';
4+
import 'package:ht_api/src/models/request_id.dart';
45
import 'package:ht_api/src/rbac/permission_service.dart';
56
import 'package:ht_api/src/registry/model_registry.dart';
67
import 'package:ht_api/src/services/auth_service.dart';
@@ -15,48 +16,6 @@ import 'package:ht_shared/ht_shared.dart';
1516
import 'package:logging/logging.dart';
1617
import 'package:uuid/uuid.dart';
1718

18-
// --- Request ID Wrapper ---
19-
20-
/// {@template request_id}
21-
/// A wrapper class holding a unique identifier (UUID v4) generated for each
22-
/// incoming HTTP request.
23-
///
24-
/// **Purpose:**
25-
/// The primary role of this ID is **traceability for logging and debugging**.
26-
/// It allows developers to follow the entire lifecycle of a *single request*
27-
/// through various middleware, route handlers, repository calls, and potential
28-
/// external service interactions by searching logs for this specific ID.
29-
/// If an error occurs during a request, this ID provides a way to isolate all
30-
/// related log entries for that specific transaction, simplifying debugging.
31-
///
32-
/// **Scope:**
33-
/// - The ID is **transient** for the request itself; it exists only during the
34-
/// request-response cycle.
35-
/// - It is **not persisted** in the main application database alongside models
36-
/// like Headlines or Categories.
37-
/// - Its value lies in being included in **persistent logs**.
38-
///
39-
/// **Distinction from other IDs:**
40-
/// - **User ID:** Identifies the authenticated user making the request. Often
41-
/// logged alongside the `request_id` for user-specific debugging.
42-
/// - **Session ID:** Tracks a user's session across multiple requests.
43-
/// - **Correlation ID:** Often generated by the *client* and passed in headers
44-
/// to link related requests initiated by the client for a larger workflow.
45-
///
46-
/// **Implementation:**
47-
/// This class ensures type safety when providing and reading the request ID
48-
/// from the Dart Frog context using `context.provide<RequestId>` and
49-
/// `context.read<RequestId>()`. This prevents potential ambiguity if other raw
50-
/// strings were provided into the context.
51-
/// {@endtemplate}
52-
class RequestId {
53-
/// {@macro request_id}
54-
const RequestId(this.id);
55-
56-
/// The unique identifier string (UUID v4).
57-
final String id;
58-
}
59-
6019
// --- Middleware Definition ---
6120
final _log = Logger('RootMiddleware');
6221

routes/api/v1/auth/anonymous.dart

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import 'dart:io';
22

33
import 'package:dart_frog/dart_frog.dart';
4+
import 'package:ht_api/src/helpers/response_helper.dart';
45
import 'package:ht_api/src/services/auth_service.dart';
56
import 'package:ht_shared/ht_shared.dart';
67

7-
import '../../../_middleware.dart';
8-
98
/// Handles POST requests to `/api/v1/auth/anonymous`.
109
///
1110
/// Creates a new anonymous user and returns the User object along with an
@@ -29,22 +28,11 @@ Future<Response> onRequest(RequestContext context) async {
2928
token: result.token,
3029
);
3130

32-
// Create metadata, including the requestId from the context.
33-
final metadata = ResponseMetadata(
34-
requestId: context.read<RequestId>().id,
35-
timestamp: DateTime.now().toUtc(),
36-
);
37-
38-
// Wrap the payload in the standard SuccessApiResponse
39-
final responsePayload = SuccessApiResponse<AuthSuccessResponse>(
31+
// Use the helper to create a standardized success response
32+
return ResponseHelper.success(
33+
context: context,
4034
data: authPayload,
41-
metadata: metadata,
42-
);
43-
44-
// Return 200 OK with the standardized, serialized response
45-
return Response.json(
46-
// Use the toJson method, providing the toJson factory for the inner type
47-
body: responsePayload.toJson((authSuccess) => authSuccess.toJson()),
35+
toJsonT: (data) => data.toJson(),
4836
);
4937
} on HtHttpException catch (_) {
5038
// Let the central errorHandler middleware handle known exceptions

0 commit comments

Comments
 (0)