@@ -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