Skip to content

Commit 3555cb4

Browse files
committed
feat(country): enhance country fetching with name filtering and caching improvements
- Add support for filtering countries by name (full or partial match) - Implement case-insensitive regex filtering for country names - Introduce caching based on usage type and name filter - Refactor country fetching methods to support name filtering - Update in-memory caches to use a map structure for improved caching
1 parent b2cc147 commit 3555cb4

File tree

1 file changed

+98
-39
lines changed

1 file changed

+98
-39
lines changed

lib/src/services/country_service.dart

Lines changed: 98 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ class CountryService {
4848
static const Duration _cacheDuration = Duration(hours: 1);
4949

5050
// In-memory caches for frequently accessed lists with time-based invalidation.
51-
_CacheEntry<List<Country>>? _cachedEventCountries;
52-
_CacheEntry<List<Country>>? _cachedHeadquarterCountries;
51+
final Map<String, _CacheEntry<List<Country>>> _cachedEventCountries = {};
52+
final Map<String, _CacheEntry<List<Country>>> _cachedHeadquarterCountries = {};
5353

5454
// Futures to hold in-flight aggregation requests to prevent cache stampedes.
5555
Future<List<Country>>? _eventCountriesFuture;
@@ -59,31 +59,48 @@ class CountryService {
5959
///
6060
/// Supports filtering by 'usage' to get countries that are either
6161
/// 'eventCountry' in headlines or 'headquarters' in sources.
62-
/// If no specific usage filter is provided, it returns all active countries.
62+
/// It also supports filtering by 'name' (full or partial match).
6363
///
6464
/// - [filter]: An optional map containing query parameters.
6565
/// Expected keys:
6666
/// - `'usage'`: String, can be 'eventCountry' or 'headquarters'.
67+
/// - `'name'`: String, a full or partial country name for search.
6768
///
6869
/// Throws [BadRequestException] if an unsupported usage filter is provided.
6970
/// Throws [OperationFailedException] for internal errors during data fetch.
7071
Future<List<Country>> getCountries(Map<String, dynamic>? filter) async {
7172
_log.info('Fetching countries with filter: $filter');
7273

7374
final usage = filter?['usage'] as String?;
75+
final name = filter?['name'] as String?;
76+
77+
Map<String, dynamic>? nameFilter;
78+
if (name != null && name.isNotEmpty) {
79+
// Create a case-insensitive regex filter for the name.
80+
nameFilter = {r'$regex': name, r'$options': 'i'};
81+
}
7482

7583
if (usage == null || usage.isEmpty) {
76-
_log.fine('No usage filter provided. Fetching all active countries.');
77-
return _getAllCountries();
84+
_log.fine(
85+
'No usage filter provided. Fetching all active countries '
86+
'with nameFilter: $nameFilter.',
87+
);
88+
return _getAllCountries(nameFilter: nameFilter);
7889
}
7990

8091
switch (usage) {
8192
case 'eventCountry':
82-
_log.fine('Fetching countries used as event countries in headlines.');
83-
return _getEventCountries();
93+
_log.fine(
94+
'Fetching countries used as event countries in headlines '
95+
'with nameFilter: $nameFilter.',
96+
);
97+
return _getEventCountries(nameFilter: nameFilter);
8498
case 'headquarters':
85-
_log.fine('Fetching countries used as headquarters in sources.');
86-
return _getHeadquarterCountries();
99+
_log.fine(
100+
'Fetching countries used as headquarters in sources '
101+
'with nameFilter: $nameFilter.',
102+
);
103+
return _getHeadquarterCountries(nameFilter: nameFilter);
87104
default:
88105
_log.warning('Unsupported country usage filter: "$usage"');
89106
throw BadRequestException(
@@ -94,15 +111,28 @@ class CountryService {
94111
}
95112

96113
/// Fetches all active countries from the repository.
97-
Future<List<Country>> _getAllCountries() async {
98-
_log.finer('Retrieving all active countries from repository.');
114+
///
115+
/// - [nameFilter]: An optional map containing a regex filter for the country name.
116+
Future<List<Country>> _getAllCountries({
117+
Map<String, dynamic>? nameFilter,
118+
}) async {
119+
_log.finer(
120+
'Retrieving all active countries from repository with nameFilter: $nameFilter.',
121+
);
99122
try {
123+
final combinedFilter = <String, dynamic>{
124+
'status': ContentStatus.active.name,
125+
};
126+
if (nameFilter != null && nameFilter.isNotEmpty) {
127+
combinedFilter.addAll({'name': nameFilter});
128+
}
129+
100130
final response = await _countryRepository.readAll(
101-
filter: {'status': ContentStatus.active.name},
131+
filter: combinedFilter,
102132
);
103133
return response.items;
104134
} catch (e, s) {
105-
_log.severe('Failed to fetch all countries.', e, s);
135+
_log.severe('Failed to fetch all countries with nameFilter: $nameFilter.', e, s);
106136
throw OperationFailedException('Failed to retrieve all countries: $e');
107137
}
108138
}
@@ -112,14 +142,20 @@ class CountryService {
112142
///
113143
/// Uses MongoDB aggregation to efficiently get distinct country IDs
114144
/// and then fetches the full Country objects. Results are cached.
115-
Future<List<Country>> _getEventCountries() async {
116-
if (_cachedEventCountries != null && _cachedEventCountries!.isValid()) {
117-
_log.finer('Returning cached event countries.');
118-
return _cachedEventCountries!.data;
145+
///
146+
/// - [nameFilter]: An optional map containing a regex filter for the country name.
147+
Future<List<Country>> _getEventCountries({
148+
Map<String, dynamic>? nameFilter,
149+
}) async {
150+
final cacheKey = 'eventCountry_${nameFilter ?? 'noFilter'}';
151+
if (_cachedEventCountries.containsKey(cacheKey) &&
152+
_cachedEventCountries[cacheKey]!.isValid()) {
153+
_log.finer('Returning cached event countries for key: $cacheKey.');
154+
return _cachedEventCountries[cacheKey]!.data;
119155
}
120156
// Atomically assign the future if no fetch is in progress,
121157
// and clear it when the future completes.
122-
_eventCountriesFuture ??= _fetchAndCacheEventCountries()
158+
_eventCountriesFuture ??= _fetchAndCacheEventCountries(nameFilter: nameFilter)
123159
.whenComplete(() => _eventCountriesFuture = null);
124160
return _eventCountriesFuture!;
125161
}
@@ -129,39 +165,54 @@ class CountryService {
129165
///
130166
/// Uses MongoDB aggregation to efficiently get distinct country IDs
131167
/// and then fetches the full Country objects. Results are cached.
132-
Future<List<Country>> _getHeadquarterCountries() async {
133-
if (_cachedHeadquarterCountries != null &&
134-
_cachedHeadquarterCountries!.isValid()) {
135-
_log.finer('Returning cached headquarter countries.');
136-
return _cachedHeadquarterCountries!.data;
168+
///
169+
/// - [nameFilter]: An optional map containing a regex filter for the country name.
170+
Future<List<Country>> _getHeadquarterCountries({
171+
Map<String, dynamic>? nameFilter,
172+
}) async {
173+
final cacheKey = 'headquarters_${nameFilter ?? 'noFilter'}';
174+
if (_cachedHeadquarterCountries.containsKey(cacheKey) &&
175+
_cachedHeadquarterCountries[cacheKey]!.isValid()) {
176+
_log.finer('Returning cached headquarter countries for key: $cacheKey.');
177+
return _cachedHeadquarterCountries[cacheKey]!.data;
137178
}
138179
// Atomically assign the future if no fetch is in progress,
139180
// and clear it when the future completes.
140-
_headquarterCountriesFuture ??= _fetchAndCacheHeadquarterCountries()
141-
.whenComplete(() => _headquarterCountriesFuture = null);
181+
_headquarterCountriesFuture ??=
182+
_fetchAndCacheHeadquarterCountries(nameFilter: nameFilter)
183+
.whenComplete(() => _headquarterCountriesFuture = null);
142184
return _headquarterCountriesFuture!;
143185
}
144186

145187
/// Helper method to fetch and cache distinct event countries.
146-
Future<List<Country>> _fetchAndCacheEventCountries() async {
147-
_log.finer('Fetching distinct event countries via aggregation.');
188+
///
189+
/// - [nameFilter]: An optional map containing a regex filter for the country name.
190+
Future<List<Country>> _fetchAndCacheEventCountries({
191+
Map<String, dynamic>? nameFilter,
192+
}) async {
193+
_log.finer(
194+
'Fetching distinct event countries via aggregation with nameFilter: $nameFilter.',
195+
);
148196
try {
149197
final distinctCountries = await _getDistinctCountriesFromAggregation(
150198
repository: _headlineRepository,
151199
fieldName: 'eventCountry',
200+
nameFilter: nameFilter,
152201
);
153-
_cachedEventCountries = _CacheEntry(
202+
final cacheKey = 'eventCountry_${nameFilter ?? 'noFilter'}';
203+
_cachedEventCountries[cacheKey] = _CacheEntry(
154204
distinctCountries,
155205
DateTime.now().add(_cacheDuration),
156206
);
157207
_log.info(
158208
'Successfully fetched and cached ${distinctCountries.length} '
159-
'event countries.',
209+
'event countries for key: $cacheKey.',
160210
);
161211
return distinctCountries;
162212
} catch (e, s) {
163213
_log.severe(
164-
'Failed to fetch distinct event countries via aggregation.',
214+
'Failed to fetch distinct event countries via aggregation '
215+
'with nameFilter: $nameFilter.',
165216
e,
166217
s,
167218
);
@@ -170,25 +221,34 @@ class CountryService {
170221
}
171222

172223
/// Helper method to fetch and cache distinct headquarter countries.
173-
Future<List<Country>> _fetchAndCacheHeadquarterCountries() async {
174-
_log.finer('Fetching distinct headquarter countries via aggregation.');
224+
///
225+
/// - [nameFilter]: An optional map containing a regex filter for the country name.
226+
Future<List<Country>> _fetchAndCacheHeadquarterCountries({
227+
Map<String, dynamic>? nameFilter,
228+
}) async {
229+
_log.finer(
230+
'Fetching distinct headquarter countries via aggregation with nameFilter: $nameFilter.',
231+
);
175232
try {
176233
final distinctCountries = await _getDistinctCountriesFromAggregation(
177234
repository: _sourceRepository,
178235
fieldName: 'headquarters',
236+
nameFilter: nameFilter,
179237
);
180-
_cachedHeadquarterCountries = _CacheEntry(
238+
final cacheKey = 'headquarters_${nameFilter ?? 'noFilter'}';
239+
_cachedHeadquarterCountries[cacheKey] = _CacheEntry(
181240
distinctCountries,
182241
DateTime.now().add(_cacheDuration),
183242
);
184243
_log.info(
185244
'Successfully fetched and cached ${distinctCountries.length} '
186-
'headquarter countries.',
245+
'headquarter countries for key: $cacheKey.',
187246
);
188247
return distinctCountries;
189248
} catch (e, s) {
190249
_log.severe(
191-
'Failed to fetch distinct headquarter countries via aggregation.',
250+
'Failed to fetch distinct headquarter countries via aggregation '
251+
'with nameFilter: $nameFilter.',
192252
e,
193253
s,
194254
);
@@ -205,7 +265,8 @@ class CountryService {
205265
/// - [nameFilter]: An optional map containing a regex filter for the country name.
206266
///
207267
/// Throws [OperationFailedException] for internal errors during data fetch.
208-
Future<List<Country>> _getDistinctCountriesFromAggregation<T extends FeedItem>({
268+
Future<List<Country>>
269+
_getDistinctCountriesFromAggregation<T extends FeedItem>({
209270
required DataRepository<T> repository,
210271
required String fieldName,
211272
Map<String, dynamic>? nameFilter,
@@ -227,9 +288,7 @@ class CountryService {
227288
// Add name filter if provided
228289
if (nameFilter != null && nameFilter.isNotEmpty) {
229290
pipeline.add({
230-
r'$match': {
231-
'$fieldName.name': nameFilter,
232-
},
291+
r'$match': {'$fieldName.name': nameFilter},
233292
});
234293
}
235294

0 commit comments

Comments
 (0)