From 92ccee96a404b0af4745aba8e209be5a203f2fcb Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:37:51 +0100 Subject: [PATCH 01/11] feat(api): create ResponseHelper for standardized success responses Introduces a new `ResponseHelper` class with a static `success` method. This helper centralizes the logic for creating standardized `SuccessApiResponse` objects, automatically populating metadata like the request ID and timestamp, and serializing the final payload into a `Response.json` object. This refactoring prepares the ground for simplifying the response-building logic in various route handlers, reducing boilerplate code and improving maintainability. --- lib/src/helpers/response_helper.dart | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 lib/src/helpers/response_helper.dart diff --git a/lib/src/helpers/response_helper.dart b/lib/src/helpers/response_helper.dart new file mode 100644 index 0000000..b9e3349 --- /dev/null +++ b/lib/src/helpers/response_helper.dart @@ -0,0 +1,43 @@ +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; +import 'package:ht_shared/ht_shared.dart'; + +import '../../routes/_middleware.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), + ); + } +} From dd2d581c37978cd7fb9c6332aa9d18bb718e9958 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:43:16 +0100 Subject: [PATCH 02/11] refactor(api): relocate RequestId to lib for proper access Moves the `RequestId` class from `routes/_middleware.dart` to a new, more appropriate location at `lib/src/models/request_id.dart`. This resolves an architectural issue where a helper class in `lib/` could not access a model defined in `routes/`. All references to `RequestId` in the root middleware and the new `ResponseHelper` have been updated to use the new, correct import path. This change improves code structure and enables further refactoring. --- lib/src/helpers/response_helper.dart | 3 +- lib/src/models/request_id.dart | 39 +++++++++++++++++++++++++ routes/_middleware.dart | 43 +--------------------------- 3 files changed, 41 insertions(+), 44 deletions(-) create mode 100644 lib/src/models/request_id.dart diff --git a/lib/src/helpers/response_helper.dart b/lib/src/helpers/response_helper.dart index b9e3349..83a8e51 100644 --- a/lib/src/helpers/response_helper.dart +++ b/lib/src/helpers/response_helper.dart @@ -1,10 +1,9 @@ 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'; -import '../../routes/_middleware.dart'; - /// A utility class to simplify the creation of standardized API responses. abstract final class ResponseHelper { /// Creates a standardized success JSON response. 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/routes/_middleware.dart b/routes/_middleware.dart index a86c088..e735873 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -1,5 +1,6 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/config/app_dependencies.dart'; +import 'package:ht_api/src/models/request_id.dart'; import 'package:ht_api/src/middlewares/error_handler.dart'; import 'package:ht_api/src/rbac/permission_service.dart'; import 'package:ht_api/src/registry/model_registry.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'); From fa93eacf10819663b0b476db83217d344c430aad Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:44:33 +0100 Subject: [PATCH 03/11] refactor(api): simplify anonymous route with ResponseHelper Updates the `/api/v1/auth/anonymous` route handler to use the new `ResponseHelper.success` method. This change removes several lines of boilerplate code related to manually creating metadata and the success response object, making the handler cleaner and more focused on its core logic. --- routes/api/v1/auth/anonymous.dart | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) 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 From c891df17d291d178b13c4ef76da4989c93566c91 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:46:00 +0100 Subject: [PATCH 04/11] refactor(api): simplify me route with ResponseHelper Updates the `/api/v1/auth/me` route handler to use the new `ResponseHelper.success` method. This change removes several lines of boilerplate code related to manually creating metadata and the success response object, making the handler cleaner and more focused on its core logic. --- routes/api/v1/auth/me.dart | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) 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())); } From e98c5fa134d8f54a2b79d8796da1477ec8a07d88 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:47:16 +0100 Subject: [PATCH 05/11] refactor(api): simplify verify-code route with ResponseHelper Updates the `/api/v1/auth/verify-code` route handler to use the new `ResponseHelper.success` method. This change removes several lines of boilerplate code related to manually creating metadata and the success response object, making the handler cleaner and more focused on its core logic. --- routes/api/v1/auth/verify-code.dart | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) 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 From c9f42d802e2b486fdbec4949b0ed3f7e577e34e5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:48:00 +0100 Subject: [PATCH 06/11] refactor(api): simplify verify-link-email route with ResponseHelper Updates the `/api/v1/auth/verify-link-email` route handler to use the new `ResponseHelper.success` method. This change removes several lines of boilerplate code related to manually creating metadata and the success response object, making the handler cleaner and more focused on its core logic. --- routes/api/v1/auth/verify-link-email.dart | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) 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; From 7f1a38bc172ce9e71e4f79c6c0fefa2fba34b27b Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:48:59 +0100 Subject: [PATCH 07/11] refactor(api): simplify data collection route with ResponseHelper Updates the `/api/v1/data/index.dart` route handler to use the new `ResponseHelper.success` method for both GET and POST requests. This change removes several lines of boilerplate code related to manually creating metadata and the success response object, making the handler cleaner and more focused on its core logic. --- routes/api/v1/data/index.dart | 47 ++++++++--------------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/routes/api/v1/data/index.dart b/routes/api/v1/data/index.dart index 82ed8d8..0045cc4 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; @@ -64,7 +62,7 @@ Future _handleGet(RequestContext context) async { }) .toList(); } catch (e) { - throw BadRequestException( + throw const BadRequestException( 'Invalid "sort" parameter format. Use "field:order,field2:order".', ); } @@ -134,24 +132,12 @@ 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()), ); - - return Response.json(body: responseJson); } /// Handles POST requests: Creates a new item in a collection. @@ -160,7 +146,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?; @@ -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(), + 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 From 1fc7c68862bc1680f32bb782cdbeff71f20196a9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:51:00 +0100 Subject: [PATCH 08/11] fix(api): add explicit cast in ResponseHelper toJsonT closure Corrects a type error in the `data/index.dart` route handler where the `toJsonT` closure passed to `ResponseHelper.success` was returning a `dynamic` type instead of the required `Map`. This change adds an explicit cast to `Map` on the result of the inner `toJson()` call, satisfying the Dart type checker and ensuring type safety. --- routes/api/v1/data/index.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/routes/api/v1/data/index.dart b/routes/api/v1/data/index.dart index 0045cc4..40fbe53 100644 --- a/routes/api/v1/data/index.dart +++ b/routes/api/v1/data/index.dart @@ -135,8 +135,9 @@ Future _handleGet(RequestContext context) async { return ResponseHelper.success( context: context, data: responseData, - toJsonT: (paginated) => (paginated as PaginatedResponse) - .toJson((item) => (item as dynamic).toJson()), + toJsonT: (paginated) => + (paginated as PaginatedResponse).toJson( + (item) => (item as dynamic).toJson() as Map), ); } @@ -209,7 +210,7 @@ Future _handlePost(RequestContext context) async { return ResponseHelper.success( context: context, data: createdItem, - toJsonT: (item) => (item as dynamic).toJson(), + toJsonT: (item) => (item as dynamic).toJson() as Map, statusCode: HttpStatus.created, ); } \ No newline at end of file From 3747b47f8245edf4d1fbaa837c28319d5f848ee1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:51:28 +0100 Subject: [PATCH 09/11] refactor(api): simplify data item route with ResponseHelper Updates the `/api/v1/data/[id]/index.dart` route handler to use the new `ResponseHelper.success` method for both GET and PUT requests. This change removes several lines of boilerplate code related to manually creating metadata and the success response object. It also removes the now-unnecessary `requestId` parameter from the internal handler functions, making the code cleaner and more focused on its core logic. --- routes/api/v1/data/[id]/index.dart | 75 +++++++----------------------- 1 file changed, 18 insertions(+), 57 deletions(-) diff --git a/routes/api/v1/data/[id]/index.dart b/routes/api/v1/data/[id]/index.dart index 2190367..c324856 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,9 +191,8 @@ 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', + 'Deserialization TypeError in PUT /data/[id]: $e', ); // Throw BadRequestException to be caught by the errorHandler throw const BadRequestException( @@ -235,8 +213,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 +224,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 +241,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 +358,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 +373,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 +384,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 +400,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 +424,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 +464,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 +532,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( From c16bc4f1bf11f26abeeecee8542865cc1ca69de7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:57:01 +0100 Subject: [PATCH 10/11] style: format --- lib/src/config/app_dependencies.dart | 32 +++++++++++------------ lib/src/config/environment_config.dart | 8 ++++-- lib/src/middlewares/error_handler.dart | 11 +++++--- lib/src/services/auth_service.dart | 19 ++++++++------ routes/api/v1/data/[id]/_middleware.dart | 1 - routes/api/v1/data/[id]/index.dart | 4 +-- routes/api/v1/data/index.dart | 33 ++++++++++++------------ 7 files changed, 58 insertions(+), 50 deletions(-) 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/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/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/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 c324856..11b90dc 100644 --- a/routes/api/v1/data/[id]/index.dart +++ b/routes/api/v1/data/[id]/index.dart @@ -191,9 +191,7 @@ Future _handlePut( itemToUpdate = modelConfig.fromJson(requestBody); } on TypeError catch (e) { // Catch errors during deserialization (e.g., missing required fields) - print( - '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).', diff --git a/routes/api/v1/data/index.dart b/routes/api/v1/data/index.dart index 40fbe53..eef39d3 100644 --- a/routes/api/v1/data/index.dart +++ b/routes/api/v1/data/index.dart @@ -50,17 +50,14 @@ 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 const BadRequestException( 'Invalid "sort" parameter format. Use "field:order,field2:order".', @@ -76,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; @@ -135,9 +133,9 @@ Future _handleGet(RequestContext context) async { return ResponseHelper.success( context: context, data: responseData, - toJsonT: (paginated) => - (paginated as PaginatedResponse).toJson( - (item) => (item as dynamic).toJson() as Map), + toJsonT: (paginated) => (paginated as PaginatedResponse).toJson( + (item) => (item as dynamic).toJson() as Map, + ), ); } @@ -164,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; @@ -213,4 +212,4 @@ Future _handlePost(RequestContext context) async { toJsonT: (item) => (item as dynamic).toJson() as Map, statusCode: HttpStatus.created, ); -} \ No newline at end of file +} From e37b24c87b40e2bcf761b544685210554b3c15bb Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:57:34 +0100 Subject: [PATCH 11/11] lint: misc --- lib/src/services/database_seeding_service.dart | 2 +- routes/_middleware.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 e735873..d93bd9d 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -1,7 +1,7 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/config/app_dependencies.dart'; -import 'package:ht_api/src/models/request_id.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';