Skip to content

Feature dashboard summary api handling #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/src/registry/model_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,27 @@ final modelRegistry = <String, ModelConfig<dynamic>>{
type: RequiredPermissionType.adminOnly, // Only administrators can delete
),
),
'dashboard_summary': ModelConfig<DashboardSummary>(
fromJson: DashboardSummary.fromJson,
getId: (summary) => summary.id,
getOwnerId: null, // Not a user-owned resource
// Permissions: Read-only for admins, all other actions unsupported.
getCollectionPermission: const ModelActionPermission(
type: RequiredPermissionType.unsupported,
),
getItemPermission: const ModelActionPermission(
type: RequiredPermissionType.adminOnly,
),
postPermission: const ModelActionPermission(
type: RequiredPermissionType.unsupported,
),
putPermission: const ModelActionPermission(
type: RequiredPermissionType.unsupported,
),
deletePermission: const ModelActionPermission(
type: RequiredPermissionType.unsupported,
),
),
};

/// Type alias for the ModelRegistry map for easier provider usage.
Expand Down
48 changes: 48 additions & 0 deletions lib/src/services/dashboard_summary_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:ht_data_repository/ht_data_repository.dart';
import 'package:ht_shared/ht_shared.dart';

/// {@template dashboard_summary_service}
/// A service responsible for calculating the dashboard summary data on demand.
///
/// This service aggregates data from various repositories to provide a
/// real-time overview of key metrics in the system.
/// {@endtemplate}
class DashboardSummaryService {
/// {@macro dashboard_summary_service}
const DashboardSummaryService({
required HtDataRepository<Headline> headlineRepository,
required HtDataRepository<Category> categoryRepository,
required HtDataRepository<Source> sourceRepository,
}) : _headlineRepository = headlineRepository,
_categoryRepository = categoryRepository,
_sourceRepository = sourceRepository;

final HtDataRepository<Headline> _headlineRepository;
final HtDataRepository<Category> _categoryRepository;
final HtDataRepository<Source> _sourceRepository;

/// Calculates and returns the current dashboard summary.
///
/// This method fetches all items from the required repositories to count them
/// and constructs a [DashboardSummary] object.
Future<DashboardSummary> getSummary() async {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The getSummary implementation fetches all items from multiple repositories to count them using readAll() followed by .items.length. This can be inefficient, especially as the number of headlines, categories, or sources grows. Consider introducing a dedicated count() method to the HtDataRepository interface to allow optimized implementations (e.g., using COUNT(*) in a database).

// Use Future.wait to fetch all counts in parallel for efficiency.
final results = await Future.wait([
_headlineRepository.readAll(),
_categoryRepository.readAll(),
_sourceRepository.readAll(),
]);

// The results are PaginatedResponse objects.
final headlineResponse = results[0] as PaginatedResponse<Headline>;
final categoryResponse = results[1] as PaginatedResponse<Category>;
final sourceResponse = results[2] as PaginatedResponse<Source>;

return DashboardSummary(
id: 'dashboard_summary', // Fixed ID for the singleton summary
headlineCount: headlineResponse.items.length,
categoryCount: categoryResponse.items.length,
sourceCount: sourceResponse.items.length,
);
Comment on lines +30 to +46

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Accessing the results of Future.wait by a numeric index (e.g., results[0]) is fragile. If the order of futures in the list changes, it can lead to runtime type errors. Using the wait extension method on a record of Futures (available in Dart 3) provides a more robust, type-safe way to handle parallel asynchronous operations.

    // Use the wait extension on a record of Futures for type-safe parallel fetching.
    final (headlineResponse, categoryResponse, sourceResponse) = await (
      _headlineRepository.readAll(),
      _categoryRepository.readAll(),
      _sourceRepository.readAll(),
    ).wait;

    return DashboardSummary(
      id: 'dashboard_summary', // Fixed ID for the singleton summary
      headlineCount: headlineResponse.items.length,
      categoryCount: categoryResponse.items.length,
      sourceCount: sourceResponse.items.length,
    );

}
}
10 changes: 10 additions & 0 deletions routes/_middleware.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:ht_api/src/registry/model_registry.dart';
import 'package:ht_api/src/services/auth_service.dart';
import 'package:ht_api/src/services/auth_token_service.dart';
import 'package:ht_api/src/services/default_user_preference_limit_service.dart';
import 'package:ht_api/src/services/dashboard_summary_service.dart';
import 'package:ht_api/src/services/jwt_auth_token_service.dart';
import 'package:ht_api/src/services/token_blacklist_service.dart';
import 'package:ht_api/src/services/user_preference_limit_service.dart';
Expand Down Expand Up @@ -175,6 +176,14 @@ Handler middleware(Handler handler) {
_createUserContentPreferencesRepository();
final appConfigRepository = _createAppConfigRepository();

// Instantiate the new DashboardSummaryService with its dependencies
final dashboardSummaryService = DashboardSummaryService(
headlineRepository: headlineRepository,
categoryRepository: categoryRepository,
sourceRepository: sourceRepository,
);
print('[MiddlewareSetup] DashboardSummaryService instantiated.');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The print statement appears to be for debugging. Remove such statements or replace them with a formal logging mechanism before merging into the main branch to avoid cluttering production logs.


const uuid = Uuid();

// --- Auth Dependencies ---
Expand Down Expand Up @@ -308,6 +317,7 @@ Handler middleware(Handler handler) {
.use(
provider<AuthService>((_) => authService),
) // Reads other services/repos
.use(provider<DashboardSummaryService>((_) => dashboardSummaryService))
// --- 5. RBAC Service Provider ---
// PURPOSE: Provides the PermissionService for authorization checks.
// ORDER: Must be provided before any middleware or handlers that use it
Expand Down
4 changes: 4 additions & 0 deletions routes/api/v1/data/[id].dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dart_frog/dart_frog.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';
import 'package:ht_api/src/services/user_preference_limit_service.dart'; // Import UserPreferenceLimitService
import 'package:ht_data_repository/ht_data_repository.dart';
import 'package:ht_shared/ht_shared.dart';
Expand Down Expand Up @@ -122,6 +123,9 @@ Future<Response> _handleGet(
id: id,
userId: userIdForRepoCall,
); // userId should be null for AppConfig
case 'dashboard_summary':
final service = context.read<DashboardSummaryService>();
item = await service.getSummary();
Comment on lines +126 to +128

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dashboard_summary is a singleton resource with a fixed ID, but this handler doesn't validate that the id from the URL path matches the expected ID ('dashboard_summary'). This could lead to confusing API behavior. Validate the ID to make the API more predictable and robust.

    case 'dashboard_summary':
      if (id != 'dashboard_summary') {
        throw NotFoundException(
          'Dashboard summary not found. The correct ID is "dashboard_summary".',
        );
      }
      final service = context.read<DashboardSummaryService>();
      item = await service.getSummary();

default:
// This case should ideally be caught by middleware, but added for safety
// Throw an exception to be caught by the errorHandler
Expand Down
Loading