diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index be39ea8..c58bb11 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -49,7 +49,7 @@ class AppDependencies { late final HtDataRepository userRepository; late final HtDataRepository userAppSettingsRepository; late final HtDataRepository - userContentPreferencesRepository; + userContentPreferencesRepository; late final HtDataRepository remoteConfigRepository; late final HtEmailRepository emailRepository; @@ -134,12 +134,12 @@ class AppDependencies { ); final userContentPreferencesClient = HtDataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'user_content_preferences', - fromJson: UserContentPreferences.fromJson, - toJson: (item) => item.toJson(), - logger: Logger('HtDataMongodb'), - ); + connectionManager: _mongoDbConnectionManager, + modelName: 'user_content_preferences', + fromJson: UserContentPreferences.fromJson, + toJson: (item) => item.toJson(), + logger: Logger('HtDataMongodb'), + ); final remoteConfigClient = HtDataMongodb( connectionManager: _mongoDbConnectionManager, modelName: 'remote_configs', @@ -154,12 +154,13 @@ class AppDependencies { sourceRepository = HtDataRepository(dataClient: sourceClient); countryRepository = HtDataRepository(dataClient: countryClient); userRepository = HtDataRepository(dataClient: userClient); - userAppSettingsRepository = - HtDataRepository(dataClient: userAppSettingsClient); - userContentPreferencesRepository = - HtDataRepository(dataClient: userContentPreferencesClient); - remoteConfigRepository = - HtDataRepository(dataClient: remoteConfigClient); + userAppSettingsRepository = HtDataRepository( + dataClient: userAppSettingsClient, + ); + userContentPreferencesRepository = HtDataRepository( + dataClient: userContentPreferencesClient, + ); + remoteConfigRepository = HtDataRepository(dataClient: remoteConfigClient); final emailClient = HtEmailInMemoryClient( logger: Logger('HtEmailInMemoryClient'), @@ -176,8 +177,7 @@ class AppDependencies { uuidGenerator: const Uuid(), log: Logger('JwtAuthTokenService'), ); - verificationCodeStorageService = - InMemoryVerificationCodeStorageService(); + verificationCodeStorageService = InMemoryVerificationCodeStorageService(); permissionService = const PermissionService(); authService = AuthService( userRepository: userRepository, @@ -219,4 +219,4 @@ class AppDependencies { _isInitialized = false; _log.info('Application dependencies disposed.'); } -} \ No newline at end of file +} diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index 0b0dc25..e26aa78 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -43,7 +43,9 @@ abstract final class EnvironmentConfig { env.load([envPath]); // Load variables from the found .env file return env; // Return immediately upon finding } else { - _log.warning('pubspec.yaml found, but no .env in the same directory.'); + _log.warning( + 'pubspec.yaml found, but no .env in the same directory.', + ); break; // Stop searching since pubspec.yaml should contain .env } } @@ -51,7 +53,9 @@ abstract final class EnvironmentConfig { _log.finer('Moving up to parent directory: ${currentDir.path}'); } // If loop completes without returning, .env was not found - _log.warning('.env not found by searching. Falling back to default load().'); + _log.warning( + '.env not found by searching. Falling back to default load().', + ); env.load(); // Fallback to default load return env; // Return even if fallback } diff --git a/lib/src/helpers/response_helper.dart b/lib/src/helpers/response_helper.dart new file mode 100644 index 0000000..83a8e51 --- /dev/null +++ b/lib/src/helpers/response_helper.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; +import 'package:ht_api/src/models/request_id.dart'; +import 'package:ht_shared/ht_shared.dart'; + +/// A utility class to simplify the creation of standardized API responses. +abstract final class ResponseHelper { + /// Creates a standardized success JSON response. + /// + /// This helper encapsulates the boilerplate of creating metadata, wrapping the + /// payload in a [SuccessApiResponse], and serializing it to JSON. + /// + /// - [context]: The request context, used to read the `RequestId`. + /// - [data]: The payload to be included in the response. + /// - [toJsonT]: A function that knows how to serialize the [data] payload. + /// This is necessary because of Dart's generics. For a simple object, this + /// would be `(data) => data.toJson()`. For a paginated list, it would be + /// `(paginated) => paginated.toJson((item) => item.toJson())`. + /// - [statusCode]: The HTTP status code for the response. Defaults to 200 OK. + static Response success({ + required RequestContext context, + required T data, + required Map Function(T data) toJsonT, + int statusCode = HttpStatus.ok, + }) { + final metadata = ResponseMetadata( + requestId: context.read().id, + timestamp: DateTime.now().toUtc(), + ); + + final responsePayload = SuccessApiResponse( + data: data, + metadata: metadata, + ); + + return Response.json( + statusCode: statusCode, + body: responsePayload.toJson(toJsonT), + ); + } +} diff --git a/lib/src/middlewares/error_handler.dart b/lib/src/middlewares/error_handler.dart index e51d0ad..f1b250b 100644 --- a/lib/src/middlewares/error_handler.dart +++ b/lib/src/middlewares/error_handler.dart @@ -29,7 +29,8 @@ Middleware errorHandler() { } on CheckedFromJsonException catch (e, stackTrace) { // Handle json_serializable validation errors. These are client errors. final field = e.key ?? 'unknown'; - final message = 'Invalid request body: Field "$field" has an ' + final message = + 'Invalid request body: Field "$field" has an ' 'invalid value or is missing. ${e.message}'; print('CheckedFromJsonException Caught: $e\n$stackTrace'); return _jsonErrorResponse( @@ -50,7 +51,9 @@ Middleware errorHandler() { print('Unhandled Exception Caught: $e\n$stackTrace'); return _jsonErrorResponse( statusCode: HttpStatus.internalServerError, // 500 - exception: const UnknownException('An unexpected internal server error occurred.'), + exception: const UnknownException( + 'An unexpected internal server error occurred.', + ), context: context, ); } @@ -137,7 +140,9 @@ Response _jsonErrorResponse({ return Response.json( statusCode: statusCode, - body: {'error': {'code': errorCode, 'message': exception.message}}, + body: { + 'error': {'code': errorCode, 'message': exception.message}, + }, headers: headers, ); } diff --git a/lib/src/models/request_id.dart b/lib/src/models/request_id.dart new file mode 100644 index 0000000..7f283a0 --- /dev/null +++ b/lib/src/models/request_id.dart @@ -0,0 +1,39 @@ +/// {@template request_id} +/// A wrapper class holding a unique identifier (UUID v4) generated for each +/// incoming HTTP request. +/// +/// **Purpose:** +/// The primary role of this ID is **traceability for logging and debugging**. +/// It allows developers to follow the entire lifecycle of a *single request* +/// through various middleware, route handlers, repository calls, and potential +/// external service interactions by searching logs for this specific ID. +/// If an error occurs during a request, this ID provides a way to isolate all +/// related log entries for that specific transaction, simplifying debugging. +/// +/// **Scope:** +/// - The ID is **transient** for the request itself; it exists only during the +/// request-response cycle. +/// - It is **not persisted** in the main application database alongside models +/// like Headlines or Categories. +/// - Its value lies in being included in **persistent logs**. +/// +/// **Distinction from other IDs:** +/// - **User ID:** Identifies the authenticated user making the request. Often +/// logged alongside the `request_id` for user-specific debugging. +/// - **Session ID:** Tracks a user's session across multiple requests. +/// - **Correlation ID:** Often generated by the *client* and passed in headers +/// to link related requests initiated by the client for a larger workflow. +/// +/// **Implementation:** +/// This class ensures type safety when providing and reading the request ID +/// from the Dart Frog context using `context.provide` and +/// `context.read()`. This prevents potential ambiguity if other raw +/// strings were provided into the context. +/// {@endtemplate} +class RequestId { + /// {@macro request_id} + const RequestId(this.id); + + /// The unique identifier string (UUID v4). + final String id; +} diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index 30a1777..0e36559 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -23,14 +23,14 @@ class AuthService { required HtEmailRepository emailRepository, required HtDataRepository userAppSettingsRepository, required HtDataRepository - userContentPreferencesRepository, + userContentPreferencesRepository, required PermissionService permissionService, required Uuid uuidGenerator, required Logger log, }) : _userRepository = userRepository, _authTokenService = authTokenService, _verificationCodeStorageService = verificationCodeStorageService, - _permissionService = permissionService, + _permissionService = permissionService, _emailRepository = emailRepository, _userAppSettingsRepository = userAppSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, @@ -43,7 +43,7 @@ class AuthService { final HtEmailRepository _emailRepository; final HtDataRepository _userAppSettingsRepository; final HtDataRepository - _userContentPreferencesRepository; + _userContentPreferencesRepository; final PermissionService _permissionService; final Logger _log; final Uuid _uuid; @@ -83,7 +83,10 @@ class AuthService { throw const UnauthorizedException( 'This email address is not registered for dashboard access.', ); - } else if (!_permissionService.hasPermission(user, Permissions.dashboardLogin)) { + } else if (!_permissionService.hasPermission( + user, + Permissions.dashboardLogin, + )) { _log.warning( 'Dashboard login failed: User ${user.id} lacks required permission (${Permissions.dashboardLogin}).', ); @@ -273,7 +276,9 @@ class AuthService { rethrow; } catch (e, s) { _log.severe( - 'Unexpected error during user lookup/creation for $email: $e', e, s, + 'Unexpected error during user lookup/creation for $email: $e', + e, + s, ); throw const OperationFailedException('Failed to process user account.'); } @@ -652,9 +657,7 @@ class AuthService { /// Re-throws any [HtHttpException] from the repository. Future _findUserByEmail(String email) async { try { - final response = await _userRepository.readAll( - filter: {'email': email}, - ); + final response = await _userRepository.readAll(filter: {'email': email}); if (response.items.isNotEmpty) { return response.items.first; } diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 86df5bf..ccb72af 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -92,7 +92,7 @@ class DatabaseSeedingService { // Filter by the specific, deterministic _id. 'filter': {'_id': objectId}, // Set the fields of the document. - 'update': {'\$set': document}, + 'update': {r'$set': document}, 'upsert': true, }, }); diff --git a/routes/_middleware.dart b/routes/_middleware.dart index a86c088..d93bd9d 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -1,6 +1,7 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/config/app_dependencies.dart'; import 'package:ht_api/src/middlewares/error_handler.dart'; +import 'package:ht_api/src/models/request_id.dart'; import 'package:ht_api/src/rbac/permission_service.dart'; import 'package:ht_api/src/registry/model_registry.dart'; import 'package:ht_api/src/services/auth_service.dart'; @@ -15,48 +16,6 @@ import 'package:ht_shared/ht_shared.dart'; import 'package:logging/logging.dart'; import 'package:uuid/uuid.dart'; -// --- Request ID Wrapper --- - -/// {@template request_id} -/// A wrapper class holding a unique identifier (UUID v4) generated for each -/// incoming HTTP request. -/// -/// **Purpose:** -/// The primary role of this ID is **traceability for logging and debugging**. -/// It allows developers to follow the entire lifecycle of a *single request* -/// through various middleware, route handlers, repository calls, and potential -/// external service interactions by searching logs for this specific ID. -/// If an error occurs during a request, this ID provides a way to isolate all -/// related log entries for that specific transaction, simplifying debugging. -/// -/// **Scope:** -/// - The ID is **transient** for the request itself; it exists only during the -/// request-response cycle. -/// - It is **not persisted** in the main application database alongside models -/// like Headlines or Categories. -/// - Its value lies in being included in **persistent logs**. -/// -/// **Distinction from other IDs:** -/// - **User ID:** Identifies the authenticated user making the request. Often -/// logged alongside the `request_id` for user-specific debugging. -/// - **Session ID:** Tracks a user's session across multiple requests. -/// - **Correlation ID:** Often generated by the *client* and passed in headers -/// to link related requests initiated by the client for a larger workflow. -/// -/// **Implementation:** -/// This class ensures type safety when providing and reading the request ID -/// from the Dart Frog context using `context.provide` and -/// `context.read()`. This prevents potential ambiguity if other raw -/// strings were provided into the context. -/// {@endtemplate} -class RequestId { - /// {@macro request_id} - const RequestId(this.id); - - /// The unique identifier string (UUID v4). - final String id; -} - // --- Middleware Definition --- final _log = Logger('RootMiddleware'); diff --git a/routes/api/v1/auth/anonymous.dart b/routes/api/v1/auth/anonymous.dart index 4a4a05b..7827b36 100644 --- a/routes/api/v1/auth/anonymous.dart +++ b/routes/api/v1/auth/anonymous.dart @@ -1,11 +1,10 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; +import 'package:ht_api/src/helpers/response_helper.dart'; import 'package:ht_api/src/services/auth_service.dart'; import 'package:ht_shared/ht_shared.dart'; -import '../../../_middleware.dart'; - /// Handles POST requests to `/api/v1/auth/anonymous`. /// /// Creates a new anonymous user and returns the User object along with an @@ -29,22 +28,11 @@ Future onRequest(RequestContext context) async { token: result.token, ); - // Create metadata, including the requestId from the context. - final metadata = ResponseMetadata( - requestId: context.read().id, - timestamp: DateTime.now().toUtc(), - ); - - // Wrap the payload in the standard SuccessApiResponse - final responsePayload = SuccessApiResponse( + // Use the helper to create a standardized success response + return ResponseHelper.success( + context: context, data: authPayload, - metadata: metadata, - ); - - // Return 200 OK with the standardized, serialized response - return Response.json( - // Use the toJson method, providing the toJson factory for the inner type - body: responsePayload.toJson((authSuccess) => authSuccess.toJson()), + toJsonT: (data) => data.toJson(), ); } on HtHttpException catch (_) { // Let the central errorHandler middleware handle known exceptions diff --git a/routes/api/v1/auth/me.dart b/routes/api/v1/auth/me.dart index 790c740..dcdb000 100644 --- a/routes/api/v1/auth/me.dart +++ b/routes/api/v1/auth/me.dart @@ -1,11 +1,9 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; -// To read RequestId if needed +import 'package:ht_api/src/helpers/response_helper.dart'; import 'package:ht_shared/ht_shared.dart'; // For User, SuccessApiResponse etc. -import '../../../_middleware.dart'; // Potentially for RequestId definition - /// Handles GET requests to `/api/v1/auth/me`. /// /// Retrieves the details of the currently authenticated user based on the @@ -30,18 +28,10 @@ Future onRequest(RequestContext context) async { throw const UnauthorizedException('Authentication required.'); } - // Create metadata, including the requestId from the context. - final metadata = ResponseMetadata( - requestId: context.read().id, - timestamp: DateTime.now().toUtc(), - ); - - // Wrap the user data in SuccessApiResponse - final responsePayload = SuccessApiResponse( + // Use the helper to create a standardized success response + return ResponseHelper.success( + context: context, data: user, - metadata: metadata, + toJsonT: (data) => data.toJson(), ); - - // Return 200 OK with the wrapped and serialized response - return Response.json(body: responsePayload.toJson((user) => user.toJson())); } diff --git a/routes/api/v1/auth/verify-code.dart b/routes/api/v1/auth/verify-code.dart index 09d42ec..2912848 100644 --- a/routes/api/v1/auth/verify-code.dart +++ b/routes/api/v1/auth/verify-code.dart @@ -1,12 +1,11 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; +import 'package:ht_api/src/helpers/response_helper.dart'; import 'package:ht_api/src/services/auth_service.dart'; // Import exceptions, User, SuccessApiResponse, AND AuthSuccessResponse import 'package:ht_shared/ht_shared.dart'; -import '../../../_middleware.dart'; - /// Handles POST requests to `/api/v1/auth/verify-code`. /// /// Verifies the provided email and code, completes the sign-in/sign-up, @@ -86,22 +85,11 @@ Future onRequest(RequestContext context) async { token: result.token, ); - // Create metadata, including the requestId from the context. - final metadata = ResponseMetadata( - requestId: context.read().id, - timestamp: DateTime.now().toUtc(), - ); - - // Wrap the payload in the standard SuccessApiResponse - final responsePayload = SuccessApiResponse( + // Use the helper to create a standardized success response + return ResponseHelper.success( + context: context, data: authPayload, - metadata: metadata, - ); - - // Return 200 OK with the standardized, serialized response - return Response.json( - // Use the toJson method, providing the toJson factory for the inner type - body: responsePayload.toJson((authSuccess) => authSuccess.toJson()), + toJsonT: (data) => data.toJson(), ); } on HtHttpException catch (_) { // Let the central errorHandler middleware handle known exceptions diff --git a/routes/api/v1/auth/verify-link-email.dart b/routes/api/v1/auth/verify-link-email.dart index 71a4c03..60a2be9 100644 --- a/routes/api/v1/auth/verify-link-email.dart +++ b/routes/api/v1/auth/verify-link-email.dart @@ -1,11 +1,10 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; +import 'package:ht_api/src/helpers/response_helper.dart'; import 'package:ht_api/src/services/auth_service.dart'; import 'package:ht_shared/ht_shared.dart'; -import '../../../_middleware.dart'; - /// Handles POST requests to `/api/v1/auth/verify-link-email`. /// /// Allows an authenticated anonymous user to complete the email linking process @@ -90,21 +89,11 @@ Future onRequest(RequestContext context) async { token: result.token, ); - // Create metadata, including the requestId from the context. - final metadata = ResponseMetadata( - requestId: context.read().id, - timestamp: DateTime.now().toUtc(), - ); - - // Wrap the payload in the standard SuccessApiResponse - final responsePayload = SuccessApiResponse( + // Use the helper to create a standardized success response + return ResponseHelper.success( + context: context, data: authPayload, - metadata: metadata, - ); - - // Return 200 OK with the standardized, serialized response - return Response.json( - body: responsePayload.toJson((authSuccess) => authSuccess.toJson()), + toJsonT: (data) => data.toJson(), ); } on HtHttpException catch (_) { rethrow; diff --git a/routes/api/v1/data/[id]/_middleware.dart b/routes/api/v1/data/[id]/_middleware.dart index 3f1c5c2..216a2f1 100644 --- a/routes/api/v1/data/[id]/_middleware.dart +++ b/routes/api/v1/data/[id]/_middleware.dart @@ -16,4 +16,3 @@ Handler middleware(Handler handler) { // `/api/v1/data/_middleware.dart` (authn, authz, model validation). return handler.use(ownershipCheckMiddleware()); } - diff --git a/routes/api/v1/data/[id]/index.dart b/routes/api/v1/data/[id]/index.dart index 2190367..11b90dc 100644 --- a/routes/api/v1/data/[id]/index.dart +++ b/routes/api/v1/data/[id]/index.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; +import 'package:ht_api/src/helpers/response_helper.dart'; import 'package:ht_api/src/rbac/permission_service.dart'; import 'package:ht_api/src/registry/model_registry.dart'; import 'package:ht_api/src/services/dashboard_summary_service.dart'; @@ -8,15 +9,12 @@ import 'package:ht_api/src/services/user_preference_limit_service.dart'; // Impo import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; -import '../../../../_middleware.dart'; // Assuming RequestId is here - /// Handles requests for the /api/v1/data/[id] endpoint. /// Dispatches requests to specific handlers based on the HTTP method. Future onRequest(RequestContext context, String id) async { // Read dependencies provided by middleware final modelName = context.read(); final modelConfig = context.read>(); - final requestId = context.read().id; // User is guaranteed non-null by requireAuthentication() middleware final authenticatedUser = context.read(); final permissionService = context @@ -35,7 +33,6 @@ Future onRequest(RequestContext context, String id) async { modelConfig, authenticatedUser, permissionService, // Pass PermissionService - requestId, ); case HttpMethod.put: return _handlePut( @@ -46,7 +43,6 @@ Future onRequest(RequestContext context, String id) async { authenticatedUser, permissionService, // Pass PermissionService userPreferenceLimitService, // Pass the limit service - requestId, ); case HttpMethod.delete: return _handleDelete( @@ -56,7 +52,6 @@ Future onRequest(RequestContext context, String id) async { modelConfig, authenticatedUser, permissionService, // Pass PermissionService - requestId, ); default: // Methods not allowed on the item endpoint @@ -74,7 +69,6 @@ Future _handleGet( ModelConfig modelConfig, User authenticatedUser, PermissionService permissionService, - String requestId, ) async { // Authorization check is handled by authorizationMiddleware before this. // This handler only needs to perform the ownership check if required. @@ -143,7 +137,7 @@ Future _handleGet( // Ensure getOwnerId is provided for models requiring ownership check if (modelConfig.getOwnerId == null) { print( - '[ReqID: $requestId] Configuration Error: Model "$modelName" requires ' + 'Configuration Error: Model "$modelName" requires ' 'ownership check for GET item but getOwnerId is not provided.', ); // Throw an exception to be caught by the errorHandler @@ -162,25 +156,11 @@ Future _handleGet( } } - // Create metadata including the request ID and current timestamp - final metadata = ResponseMetadata( - requestId: requestId, - timestamp: DateTime.now().toUtc(), // Use UTC for consistency - ); - - // Wrap the item in SuccessApiResponse with metadata - final successResponse = SuccessApiResponse( + return ResponseHelper.success( + context: context, data: item, - metadata: metadata, // Include the created metadata + toJsonT: (data) => (data as dynamic).toJson() as Map, ); - - // Provide the correct toJsonT for the specific model type - final responseJson = successResponse.toJson( - (item) => (item as dynamic).toJson(), // Assuming all models have toJson - ); - - // Return 200 OK with the wrapped and serialized response - return Response.json(body: responseJson); } // --- PUT Handler --- @@ -195,7 +175,6 @@ Future _handlePut( PermissionService permissionService, // Receive PermissionService UserPreferenceLimitService userPreferenceLimitService, // Receive Limit Service - String requestId, ) async { // Authorization check is handled by authorizationMiddleware before this. // This handler only needs to perform the ownership check if required. @@ -212,10 +191,7 @@ Future _handlePut( itemToUpdate = modelConfig.fromJson(requestBody); } on TypeError catch (e) { // Catch errors during deserialization (e.g., missing required fields) - // Include requestId in the server log - print( - '[ReqID: $requestId] Deserialization TypeError in PUT /data/[id]: $e', - ); + print('Deserialization TypeError in PUT /data/[id]: $e'); // Throw BadRequestException to be caught by the errorHandler throw const BadRequestException( 'Invalid request body: Missing or invalid required field(s).', @@ -235,8 +211,7 @@ Future _handlePut( } catch (e) { // Ignore if getId throws, means ID might not be in the body, // which is acceptable depending on the model/client. - // Log for debugging if needed. - print('[ReqID: $requestId] Warning: Could not get ID from PUT body: $e'); + print('Warning: Could not get ID from PUT body: $e'); } // --- Handler-Level Limit Check (for UserContentPreferences PUT) --- @@ -247,7 +222,7 @@ Future _handlePut( // Ensure the itemToUpdate is the correct type for the limit service if (itemToUpdate is! UserContentPreferences) { print( - '[ReqID: $requestId] Type Error: Expected UserContentPreferences ' + 'Type Error: Expected UserContentPreferences ' 'for limit check, but got ${itemToUpdate.runtimeType}.', ); throw const OperationFailedException( @@ -264,7 +239,7 @@ Future _handlePut( } catch (e) { // Catch unexpected errors from the limit service print( - '[ReqID: $requestId] Unexpected error during limit check for ' + 'Unexpected error during limit check for ' 'UserContentPreferences PUT: $e', ); throw const OperationFailedException( @@ -381,7 +356,7 @@ Future _handlePut( // Ensure getOwnerId is provided for models requiring ownership check if (modelConfig.getOwnerId == null) { print( - '[ReqID: $requestId] Configuration Error: Model "$modelName" requires ' + 'Configuration Error: Model "$modelName" requires ' 'ownership check for PUT but getOwnerId is not provided.', ); // Throw an exception to be caught by the errorHandler @@ -396,9 +371,8 @@ Future _handlePut( if (itemOwnerId != authenticatedUser.id) { // This scenario should ideally not happen if the repository correctly // enforced ownership during the update call when userId was passed. - // But as a defense-in-depth, we check here. print( - '[ReqID: $requestId] Ownership check failed AFTER PUT for item $id. ' + 'Ownership check failed AFTER PUT for item $id. ' 'Item owner: $itemOwnerId, User: ${authenticatedUser.id}', ); // Throw ForbiddenException to be caught by the errorHandler @@ -408,25 +382,11 @@ Future _handlePut( } } - // Create metadata including the request ID and current timestamp - final metadata = ResponseMetadata( - requestId: requestId, - timestamp: DateTime.now().toUtc(), // Use UTC for consistency - ); - - // Wrap the updated item in SuccessApiResponse with metadata - final successResponse = SuccessApiResponse( + return ResponseHelper.success( + context: context, data: updatedItem, - metadata: metadata, + toJsonT: (data) => (data as dynamic).toJson() as Map, ); - - // Provide the correct toJsonT for the specific model type - final responseJson = successResponse.toJson( - (item) => (item as dynamic).toJson(), // Assuming all models have toJson - ); - - // Return 200 OK with the wrapped and serialized response - return Response.json(body: responseJson); } // --- DELETE Handler --- @@ -438,7 +398,6 @@ Future _handleDelete( ModelConfig modelConfig, User authenticatedUser, PermissionService permissionService, - String requestId, ) async { // Authorization check is handled by authorizationMiddleware before this. // This handler only needs to perform the ownership check if required. @@ -463,7 +422,7 @@ Future _handleDelete( // Ensure getOwnerId is provided for models requiring ownership check if (modelConfig.getOwnerId == null) { print( - '[ReqID: $requestId] Configuration Error: Model "$modelName" requires ' + 'Configuration Error: Model "$modelName" requires ' 'ownership check for DELETE but getOwnerId is not provided.', ); // Throw an exception to be caught by the errorHandler @@ -503,7 +462,7 @@ Future _handleDelete( ); // userId should be null for AppConfig default: print( - '[ReqID: $requestId] Error: Unsupported model type "$modelName" reached _handleDelete ownership check.', + 'Error: Unsupported model type "$modelName" reached _handleDelete ownership check.', ); // Throw an exception to be caught by the errorHandler throw OperationFailedException( @@ -571,9 +530,9 @@ Future _handleDelete( ); // userId should be null for AppConfig default: // This case should ideally be caught by the data/_middleware.dart, - // but added for safety. Consider logging this unexpected state. + // but added for safety. print( - '[ReqID: $requestId] Error: Unsupported model type "$modelName" reached _handleDelete.', + 'Error: Unsupported model type "$modelName" reached _handleDelete.', ); // Throw an exception to be caught by the errorHandler throw OperationFailedException( diff --git a/routes/api/v1/data/index.dart b/routes/api/v1/data/index.dart index 82ed8d8..eef39d3 100644 --- a/routes/api/v1/data/index.dart +++ b/routes/api/v1/data/index.dart @@ -2,13 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; +import 'package:ht_api/src/helpers/response_helper.dart'; +import 'package:ht_api/src/rbac/permission_service.dart'; import 'package:ht_api/src/registry/model_registry.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; -import 'package:ht_api/src/rbac/permission_service.dart'; import 'package:ht_shared/ht_shared.dart'; -import '../../../_middleware.dart'; // For RequestId - /// Handles requests for the /api/v1/data collection endpoint. /// Dispatches requests to specific handlers based on the HTTP method. Future onRequest(RequestContext context) async { @@ -31,7 +30,6 @@ Future _handleGet(RequestContext context) async { final modelName = context.read(); final modelConfig = context.read>(); final authenticatedUser = context.read(); - final requestId = context.read().id; // --- Parse Query Parameters --- final params = context.request.uri.queryParameters; @@ -52,19 +50,16 @@ Future _handleGet(RequestContext context) async { List? sort; if (params.containsKey('sort')) { try { - sort = params['sort']! - .split(',') - .map((s) { - final parts = s.split(':'); - final field = parts[0]; - final order = (parts.length > 1 && parts[1] == 'desc') - ? SortOrder.desc - : SortOrder.asc; - return SortOption(field, order); - }) - .toList(); + sort = params['sort']!.split(',').map((s) { + final parts = s.split(':'); + final field = parts[0]; + final order = (parts.length > 1 && parts[1] == 'desc') + ? SortOrder.desc + : SortOrder.asc; + return SortOption(field, order); + }).toList(); } catch (e) { - throw BadRequestException( + throw const BadRequestException( 'Invalid "sort" parameter format. Use "field:order,field2:order".', ); } @@ -78,7 +73,8 @@ Future _handleGet(RequestContext context) async { } // --- Repository Call --- - final userIdForRepoCall = (modelConfig.getOwnerId != null && + final userIdForRepoCall = + (modelConfig.getOwnerId != null && !context.read().isAdmin(authenticatedUser)) ? authenticatedUser.id : null; @@ -134,24 +130,13 @@ Future _handleGet(RequestContext context) async { ); } - // --- Format and Return Response --- - final metadata = ResponseMetadata( - requestId: requestId, - timestamp: DateTime.now().toUtc(), - ); - - final successResponse = SuccessApiResponse( + return ResponseHelper.success( + context: context, data: responseData, - metadata: metadata, - ); - - final responseJson = successResponse.toJson( - (paginated) => (paginated as PaginatedResponse).toJson( - (item) => (item as dynamic).toJson(), + toJsonT: (paginated) => (paginated as PaginatedResponse).toJson( + (item) => (item as dynamic).toJson() as Map, ), ); - - return Response.json(body: responseJson); } /// Handles POST requests: Creates a new item in a collection. @@ -160,7 +145,6 @@ Future _handlePost(RequestContext context) async { final modelName = context.read(); final modelConfig = context.read>(); final authenticatedUser = context.read(); - final requestId = context.read().id; // --- Parse Body --- final requestBody = await context.request.json() as Map?; @@ -178,7 +162,8 @@ Future _handlePost(RequestContext context) async { } // --- Repository Call --- - final userIdForRepoCall = (modelConfig.getOwnerId != null && + final userIdForRepoCall = + (modelConfig.getOwnerId != null && !context.read().isAdmin(authenticatedUser)) ? authenticatedUser.id : null; @@ -221,20 +206,10 @@ Future _handlePost(RequestContext context) async { ); } - // --- Format and Return Response --- - final metadata = ResponseMetadata( - requestId: requestId, - timestamp: DateTime.now().toUtc(), - ); - - final successResponse = SuccessApiResponse( + return ResponseHelper.success( + context: context, data: createdItem, - metadata: metadata, + toJsonT: (item) => (item as dynamic).toJson() as Map, + statusCode: HttpStatus.created, ); - - final responseJson = successResponse.toJson( - (item) => (item as dynamic).toJson(), - ); - - return Response.json(statusCode: HttpStatus.created, body: responseJson); -} \ No newline at end of file +}