Skip to content

Commit 56df6b6

Browse files
authored
Merge pull request #44 from flutter-news-app-full-source-code/Enhanced-Country-Data-Route
Enhanced country data route
2 parents 6d86824 + 418d3e6 commit 56df6b6

File tree

4 files changed

+219
-7
lines changed

4 files changed

+219
-7
lines changed

lib/src/config/app_dependencies.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter_news_app_api_server_full_source_code/src/config/environm
99
import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart';
1010
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart';
1111
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_token_service.dart';
12+
import 'package:flutter_news_app_api_server_full_source_code/src/services/country_service.dart';
1213
import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart';
1314
import 'package:flutter_news_app_api_server_full_source_code/src/services/database_seeding_service.dart';
1415
import 'package:flutter_news_app_api_server_full_source_code/src/services/default_user_preference_limit_service.dart';
@@ -61,6 +62,7 @@ class AppDependencies {
6162
late final EmailRepository emailRepository;
6263

6364
// Services
65+
late final CountryService countryService;
6466
late final TokenBlacklistService tokenBlacklistService;
6567
late final AuthTokenService authTokenService;
6668
late final VerificationCodeStorageService verificationCodeStorageService;
@@ -179,7 +181,6 @@ class AppDependencies {
179181
dataClient: userContentPreferencesClient,
180182
);
181183
remoteConfigRepository = DataRepository(dataClient: remoteConfigClient);
182-
183184
// Configure the HTTP client for SendGrid.
184185
// The HttpClient's AuthInterceptor will use the tokenProvider to add
185186
// the 'Authorization: Bearer <SENDGRID_API_KEY>' header.
@@ -238,6 +239,12 @@ class AppDependencies {
238239
connectionManager: _mongoDbConnectionManager,
239240
log: Logger('MongoDbRateLimitService'),
240241
);
242+
countryService = CountryService(
243+
countryRepository: countryRepository,
244+
headlineRepository: headlineRepository,
245+
sourceRepository: sourceRepository,
246+
logger: Logger('CountryService'),
247+
);
241248

242249
_isInitialized = true;
243250
_log.info('Application dependencies initialized successfully.');

lib/src/registry/data_operation_registry.dart

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:core/core.dart';
22
import 'package:dart_frog/dart_frog.dart';
33
import 'package:data_repository/data_repository.dart';
44
import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/ownership_check_middleware.dart';
5+
import 'package:flutter_news_app_api_server_full_source_code/src/services/country_service.dart';
56
import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart';
67

78
// --- Typedefs for Data Operations ---
@@ -128,12 +129,19 @@ class DataOperationRegistry {
128129
sort: s,
129130
pagination: p,
130131
),
131-
'country': (c, uid, f, s, p) => c.read<DataRepository<Country>>().readAll(
132-
userId: uid,
133-
filter: f,
134-
sort: s,
135-
pagination: p,
136-
),
132+
'country': (c, uid, f, s, p) async {
133+
// For 'country' model, delegate to CountryService for specialized filtering.
134+
// The CountryService handles the 'usage' filter and returns a List<Country>.
135+
// We then wrap this list in a PaginatedResponse for consistency with
136+
// the generic API response structure.
137+
final countryService = c.read<CountryService>();
138+
final countries = await countryService.getCountries(f);
139+
return PaginatedResponse<Country>(
140+
items: countries,
141+
cursor: null, // No cursor for this type of filtered list
142+
hasMore: false, // No more items as it's a complete filtered set
143+
);
144+
},
137145
'language': (c, uid, f, s, p) => c
138146
.read<DataRepository<Language>>()
139147
.readAll(userId: uid, filter: f, sort: s, pagination: p),
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import 'package:core/core.dart';
2+
import 'package:data_repository/data_repository.dart';
3+
import 'package:logging/logging.dart';
4+
5+
/// {@template country_service}
6+
/// A service responsible for retrieving country data, including specialized
7+
/// lists like countries associated with headlines or sources.
8+
///
9+
/// This service leverages database aggregation for efficient data retrieval
10+
/// and includes basic in-memory caching to optimize performance for frequently
11+
/// requested lists.
12+
/// {@endtemplate}
13+
class CountryService {
14+
/// {@macro country_service}
15+
CountryService({
16+
required DataRepository<Country> countryRepository,
17+
required DataRepository<Headline> headlineRepository,
18+
required DataRepository<Source> sourceRepository,
19+
Logger? logger,
20+
}) : _countryRepository = countryRepository,
21+
_headlineRepository = headlineRepository,
22+
_sourceRepository = sourceRepository,
23+
_log = logger ?? Logger('CountryService');
24+
25+
final DataRepository<Country> _countryRepository;
26+
final DataRepository<Headline> _headlineRepository;
27+
final DataRepository<Source> _sourceRepository;
28+
final Logger _log;
29+
30+
// In-memory caches for frequently accessed lists.
31+
// These should be cleared periodically in a real-world application
32+
// or invalidated upon data changes. For this scope, simple caching is used.
33+
List<Country>? _cachedEventCountries;
34+
List<Country>? _cachedHeadquarterCountries;
35+
36+
/// Retrieves a list of countries based on the provided filter.
37+
///
38+
/// Supports filtering by 'usage' to get countries that are either
39+
/// 'eventCountry' in headlines or 'headquarters' in sources.
40+
/// If no specific usage filter is provided, it returns all active countries.
41+
///
42+
/// - [filter]: An optional map containing query parameters.
43+
/// Expected keys:
44+
/// - `'usage'`: String, can be 'eventCountry' or 'headquarters'.
45+
///
46+
/// Throws [BadRequestException] if an unsupported usage filter is provided.
47+
/// Throws [OperationFailedException] for internal errors during data fetch.
48+
Future<List<Country>> getCountries(Map<String, dynamic>? filter) async {
49+
_log.info('Fetching countries with filter: $filter');
50+
51+
final usage = filter?['usage'] as String?;
52+
53+
if (usage == null || usage.isEmpty) {
54+
_log.fine('No usage filter provided. Fetching all active countries.');
55+
return _getAllCountries();
56+
}
57+
58+
switch (usage) {
59+
case 'eventCountry':
60+
_log.fine('Fetching countries used as event countries in headlines.');
61+
return _getEventCountries();
62+
case 'headquarters':
63+
_log.fine('Fetching countries used as headquarters in sources.');
64+
return _getHeadquarterCountries();
65+
default:
66+
_log.warning('Unsupported country usage filter: "$usage"');
67+
throw BadRequestException(
68+
'Unsupported country usage filter: "$usage". '
69+
'Supported values are "eventCountry" and "headquarters".',
70+
);
71+
}
72+
}
73+
74+
/// Fetches all active countries from the repository.
75+
Future<List<Country>> _getAllCountries() async {
76+
_log.finer('Retrieving all active countries from repository.');
77+
try {
78+
final response = await _countryRepository.readAll(
79+
filter: {'status': ContentStatus.active.name},
80+
);
81+
return response.items;
82+
} catch (e, s) {
83+
_log.severe('Failed to fetch all countries.', e, s);
84+
throw OperationFailedException('Failed to retrieve all countries: $e');
85+
}
86+
}
87+
88+
/// Fetches a distinct list of countries that are referenced as
89+
/// `eventCountry` in headlines.
90+
///
91+
/// Uses MongoDB aggregation to efficiently get distinct country IDs
92+
/// and then fetches the full Country objects. Results are cached.
93+
Future<List<Country>> _getEventCountries() async {
94+
if (_cachedEventCountries != null) {
95+
_log.finer('Returning cached event countries.');
96+
return _cachedEventCountries!;
97+
}
98+
99+
_log.finer('Fetching distinct event countries via aggregation.');
100+
try {
101+
final pipeline = [
102+
{
103+
r'$match': {
104+
'status': ContentStatus.active.name,
105+
'eventCountry.id': {r'$exists': true},
106+
},
107+
},
108+
{
109+
r'$group': {
110+
'_id': r'$eventCountry.id',
111+
'country': {r'$first': r'$eventCountry'},
112+
},
113+
},
114+
{
115+
r'$replaceRoot': {'newRoot': r'$country'},
116+
},
117+
];
118+
119+
final distinctCountriesJson = await _headlineRepository.aggregate(
120+
pipeline: pipeline,
121+
);
122+
123+
final distinctCountries = distinctCountriesJson
124+
.map(Country.fromJson)
125+
.toList();
126+
127+
_cachedEventCountries = distinctCountries;
128+
_log.info(
129+
'Successfully fetched and cached ${distinctCountries.length} '
130+
'event countries.',
131+
);
132+
return distinctCountries;
133+
} catch (e, s) {
134+
_log.severe('Failed to fetch event countries via aggregation.', e, s);
135+
throw OperationFailedException('Failed to retrieve event countries: $e');
136+
}
137+
}
138+
139+
/// Fetches a distinct list of countries that are referenced as
140+
/// `headquarters` in sources.
141+
///
142+
/// Uses MongoDB aggregation to efficiently get distinct country IDs
143+
/// and then fetches the full Country objects. Results are cached.
144+
Future<List<Country>> _getHeadquarterCountries() async {
145+
if (_cachedHeadquarterCountries != null) {
146+
_log.finer('Returning cached headquarter countries.');
147+
return _cachedHeadquarterCountries!;
148+
}
149+
150+
_log.finer('Fetching distinct headquarter countries via aggregation.');
151+
try {
152+
final pipeline = [
153+
{
154+
r'$match': {
155+
'status': ContentStatus.active.name,
156+
'headquarters.id': {r'$exists': true},
157+
},
158+
},
159+
{
160+
r'$group': {
161+
'_id': r'$headquarters.id',
162+
'country': {r'$first': r'$headquarters'},
163+
},
164+
},
165+
{
166+
r'$replaceRoot': {'newRoot': r'$country'},
167+
},
168+
];
169+
170+
final distinctCountriesJson = await _sourceRepository.aggregate(
171+
pipeline: pipeline,
172+
);
173+
174+
final distinctCountries = distinctCountriesJson
175+
.map(Country.fromJson)
176+
.toList();
177+
178+
_cachedHeadquarterCountries = distinctCountries;
179+
_log.info(
180+
'Successfully fetched and cached ${distinctCountries.length} '
181+
'headquarter countries.',
182+
);
183+
return distinctCountries;
184+
} catch (e, s) {
185+
_log.severe(
186+
'Failed to fetch headquarter countries via aggregation.',
187+
e,
188+
s,
189+
);
190+
throw OperationFailedException(
191+
'Failed to retrieve headquarter countries: $e',
192+
);
193+
}
194+
}
195+
}

routes/_middleware.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:flutter_news_app_api_server_full_source_code/src/registry/data_o
1010
import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart';
1111
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart';
1212
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_token_service.dart';
13+
import 'package:flutter_news_app_api_server_full_source_code/src/services/country_service.dart';
1314
import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart';
1415
import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart';
1516
import 'package:flutter_news_app_api_server_full_source_code/src/services/token_blacklist_service.dart';
@@ -151,6 +152,7 @@ Handler middleware(Handler handler) {
151152
),
152153
)
153154
.use(provider<RateLimitService>((_) => deps.rateLimitService))
155+
.use(provider<CountryService>((_) => deps.countryService))
154156
.call(context);
155157
};
156158
});

0 commit comments

Comments
 (0)