Skip to content

Commit 4c638c0

Browse files
committed
feat(service): implement CountryService for efficient country data retrieval
- Add CountryService class to handle country data operations - Implement methods to fetch countries based on different usage filters - Use in-memory caching for frequently accessed lists - Leverage database aggregation for efficient data retrieval - Include error handling for unsupported filters and data fetch failures
1 parent 6d86824 commit 4c638c0

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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(
85+
'Failed to retrieve all countries: ${e.toString()}',
86+
);
87+
}
88+
}
89+
90+
/// Fetches a distinct list of countries that are referenced as
91+
/// `eventCountry` in headlines.
92+
///
93+
/// Uses MongoDB aggregation to efficiently get distinct country IDs
94+
/// and then fetches the full Country objects. Results are cached.
95+
Future<List<Country>> _getEventCountries() async {
96+
if (_cachedEventCountries != null) {
97+
_log.finer('Returning cached event countries.');
98+
return _cachedEventCountries!;
99+
}
100+
101+
_log.finer('Fetching distinct event countries via aggregation.');
102+
try {
103+
final pipeline = [
104+
{
105+
r'$match': {
106+
'status': ContentStatus.active.name,
107+
'eventCountry.id': {r'$exists': true},
108+
},
109+
},
110+
{
111+
r'$group': {
112+
'_id': r'$eventCountry.id',
113+
'country': {r'$first': r'$eventCountry'},
114+
},
115+
},
116+
{r'$replaceRoot': {'newRoot': r'$country'}},
117+
];
118+
119+
final distinctCountriesJson =
120+
await _headlineRepository.aggregate(pipeline: pipeline);
121+
122+
final distinctCountries = distinctCountriesJson
123+
.map((json) => Country.fromJson(json))
124+
.toList();
125+
126+
_cachedEventCountries = distinctCountries;
127+
_log.info('Successfully fetched and cached ${distinctCountries.length} '
128+
'event countries.');
129+
return distinctCountries;
130+
} catch (e, s) {
131+
_log.severe('Failed to fetch event countries via aggregation.', e, s);
132+
throw OperationFailedException(
133+
'Failed to retrieve event countries: ${e.toString()}',
134+
);
135+
}
136+
}
137+
138+
/// Fetches a distinct list of countries that are referenced as
139+
/// `headquarters` in sources.
140+
///
141+
/// Uses MongoDB aggregation to efficiently get distinct country IDs
142+
/// and then fetches the full Country objects. Results are cached.
143+
Future<List<Country>> _getHeadquarterCountries() async {
144+
if (_cachedHeadquarterCountries != null) {
145+
_log.finer('Returning cached headquarter countries.');
146+
return _cachedHeadquarterCountries!;
147+
}
148+
149+
_log.finer('Fetching distinct headquarter countries via aggregation.');
150+
try {
151+
final pipeline = [
152+
{
153+
r'$match': {
154+
'status': ContentStatus.active.name,
155+
'headquarters.id': {r'$exists': true},
156+
},
157+
},
158+
{
159+
r'$group': {
160+
'_id': r'$headquarters.id',
161+
'country': {r'$first': r'$headquarters'},
162+
},
163+
},
164+
{r'$replaceRoot': {'newRoot': r'$country'}},
165+
];
166+
167+
final distinctCountriesJson =
168+
await _sourceRepository.aggregate(pipeline: pipeline);
169+
170+
final distinctCountries = distinctCountriesJson
171+
.map((json) => Country.fromJson(json))
172+
.toList();
173+
174+
_cachedHeadquarterCountries = distinctCountries;
175+
_log.info('Successfully fetched and cached ${distinctCountries.length} '
176+
'headquarter countries.');
177+
return distinctCountries;
178+
} catch (e, s) {
179+
_log.severe('Failed to fetch headquarter countries via aggregation.', e, s);
180+
throw OperationFailedException(
181+
'Failed to retrieve headquarter countries: ${e.toString()}',
182+
);
183+
}
184+
}
185+
}

0 commit comments

Comments
 (0)