@@ -48,42 +48,60 @@ 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 =
53+ {};
5354
5455 // Futures to hold in-flight aggregation requests to prevent cache stampedes.
55- Future <List <Country >>? _eventCountriesFuture ;
56- Future <List <Country >>? _headquarterCountriesFuture ;
56+ final Map < String , Future <List <Country >>> _eventCountriesFutures = {} ;
57+ final Map < String , Future <List <Country >>> _headquarterCountriesFutures = {} ;
5758
5859 /// Retrieves a list of countries based on the provided filter.
5960 ///
6061 /// Supports filtering by 'usage' to get countries that are either
6162 /// 'eventCountry' in headlines or 'headquarters' in sources.
62- /// If no specific usage filter is provided, it returns all active countries .
63+ /// It also supports filtering by 'name' (full or partial match) .
6364 ///
6465 /// - [filter] : An optional map containing query parameters.
6566 /// Expected keys:
6667 /// - `'usage'` : String, can be 'eventCountry' or 'headquarters'.
68+ /// - `'name'` : String, a full or partial country name for search.
6769 ///
6870 /// Throws [BadRequestException] if an unsupported usage filter is provided.
6971 /// Throws [OperationFailedException] for internal errors during data fetch.
7072 Future <List <Country >> getCountries (Map <String , dynamic >? filter) async {
7173 _log.info ('Fetching countries with filter: $filter ' );
7274
7375 final usage = filter? ['usage' ] as String ? ;
76+ final name = filter? ['name' ] as String ? ;
77+ 78+ Map <String , dynamic >? nameFilter;
79+ if (name != null && name.isNotEmpty) {
80+ // Create a case-insensitive regex filter for the name.
81+ nameFilter = {r'$regex' : name, r'$options' : 'i' };
82+ }
7483
7584 if (usage == null || usage.isEmpty) {
76- _log.fine ('No usage filter provided. Fetching all active countries.' );
77- return _getAllCountries ();
85+ _log.fine (
86+ 'No usage filter provided. Fetching all active countries '
87+ 'with nameFilter: $nameFilter .' ,
88+ );
89+ return _getAllCountries (nameFilter: nameFilter);
7890 }
7991
8092 switch (usage) {
8193 case 'eventCountry' :
82- _log.fine ('Fetching countries used as event countries in headlines.' );
83- return _getEventCountries ();
94+ _log.fine (
95+ 'Fetching countries used as event countries in headlines '
96+ 'with nameFilter: $nameFilter .' ,
97+ );
98+ return _getEventCountries (nameFilter: nameFilter);
8499 case 'headquarters' :
85- _log.fine ('Fetching countries used as headquarters in sources.' );
86- return _getHeadquarterCountries ();
100+ _log.fine (
101+ 'Fetching countries used as headquarters in sources '
102+ 'with nameFilter: $nameFilter .' ,
103+ );
104+ return _getHeadquarterCountries (nameFilter: nameFilter);
87105 default :
88106 _log.warning ('Unsupported country usage filter: "$usage "' );
89107 throw BadRequestException (
@@ -94,15 +112,30 @@ class CountryService {
94112 }
95113
96114 /// Fetches all active countries from the repository.
97- Future <List <Country >> _getAllCountries () async {
98- _log.finer ('Retrieving all active countries from repository.' );
115+ ///
116+ /// - [nameFilter] : An optional map containing a regex filter for the country name.
117+ Future <List <Country >> _getAllCountries ({
118+ Map <String , dynamic >? nameFilter,
119+ }) async {
120+ _log.finer (
121+ 'Retrieving all active countries from repository with nameFilter: $nameFilter .' ,
122+ );
99123 try {
100- final response = await _countryRepository.readAll (
101- filter: {'status' : ContentStatus .active.name},
102- );
124+ final combinedFilter = < String , dynamic > {
125+ 'status' : ContentStatus .active.name,
126+ };
127+ if (nameFilter != null && nameFilter.isNotEmpty) {
128+ combinedFilter.addAll ({'name' : nameFilter});
129+ }
130+ 131+ final response = await _countryRepository.readAll (filter: combinedFilter);
103132 return response.items;
104133 } catch (e, s) {
105- _log.severe ('Failed to fetch all countries.' , e, s);
134+ _log.severe (
135+ 'Failed to fetch all countries with nameFilter: $nameFilter .' ,
136+ e,
137+ s,
138+ );
106139 throw OperationFailedException ('Failed to retrieve all countries: $e ' );
107140 }
108141 }
@@ -112,56 +145,84 @@ class CountryService {
112145 ///
113146 /// Uses MongoDB aggregation to efficiently get distinct country IDs
114147 /// 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;
148+ ///
149+ /// - [nameFilter] : An optional map containing a regex filter for the country name.
150+ Future <List <Country >> _getEventCountries ({
151+ Map <String , dynamic >? nameFilter,
152+ }) async {
153+ final cacheKey = 'eventCountry_${nameFilter ?? 'noFilter' }' ;
154+ if (_cachedEventCountries.containsKey (cacheKey) &&
155+ _cachedEventCountries[cacheKey]! .isValid ()) {
156+ _log.finer ('Returning cached event countries for key: $cacheKey .' );
157+ return _cachedEventCountries[cacheKey]! .data;
158+ }
159+ // Atomically retrieve or create the future for the specific cache key.
160+ var future = _eventCountriesFutures[cacheKey];
161+ if (future == null ) {
162+ future = _fetchAndCacheEventCountries (
163+ nameFilter: nameFilter,
164+ ).whenComplete (() => _eventCountriesFutures.remove (cacheKey));
165+ _eventCountriesFutures[cacheKey] = future;
119166 }
120- // Atomically assign the future if no fetch is in progress,
121- // and clear it when the future completes.
122- _eventCountriesFuture ?? = _fetchAndCacheEventCountries ()
123- .whenComplete (() => _eventCountriesFuture = null );
124- return _eventCountriesFuture! ;
167+ return future;
125168 }
126169
127170 /// Fetches a distinct list of countries that are referenced as
128171 /// `headquarters` in sources.
129172 ///
130173 /// Uses MongoDB aggregation to efficiently get distinct country IDs
131174 /// 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;
175+ ///
176+ /// - [nameFilter] : An optional map containing a regex filter for the country name.
177+ Future <List <Country >> _getHeadquarterCountries ({
178+ Map <String , dynamic >? nameFilter,
179+ }) async {
180+ final cacheKey = 'headquarters_${nameFilter ?? 'noFilter' }' ;
181+ if (_cachedHeadquarterCountries.containsKey (cacheKey) &&
182+ _cachedHeadquarterCountries[cacheKey]! .isValid ()) {
183+ _log.finer ('Returning cached headquarter countries for key: $cacheKey .' );
184+ return _cachedHeadquarterCountries[cacheKey]! .data;
137185 }
138- // Atomically assign the future if no fetch is in progress,
139- // and clear it when the future completes.
140- _headquarterCountriesFuture ?? = _fetchAndCacheHeadquarterCountries ()
141- .whenComplete (() => _headquarterCountriesFuture = null );
142- return _headquarterCountriesFuture! ;
186+ // Atomically retrieve or create the future for the specific cache key.
187+ var future = _headquarterCountriesFutures[cacheKey];
188+ if (future == null ) {
189+ future = _fetchAndCacheHeadquarterCountries (
190+ nameFilter: nameFilter,
191+ ).whenComplete (() => _headquarterCountriesFutures.remove (cacheKey));
192+ _headquarterCountriesFutures[cacheKey] = future;
193+ }
194+ return future;
143195 }
144196
145197 /// Helper method to fetch and cache distinct event countries.
146- Future <List <Country >> _fetchAndCacheEventCountries () async {
147- _log.finer ('Fetching distinct event countries via aggregation.' );
198+ ///
199+ /// - [nameFilter] : An optional map containing a regex filter for the country name.
200+ Future <List <Country >> _fetchAndCacheEventCountries ({
201+ Map <String , dynamic >? nameFilter,
202+ }) async {
203+ _log.finer (
204+ 'Fetching distinct event countries via aggregation with nameFilter: $nameFilter .' ,
205+ );
148206 try {
149207 final distinctCountries = await _getDistinctCountriesFromAggregation (
150208 repository: _headlineRepository,
151209 fieldName: 'eventCountry' ,
210+ nameFilter: nameFilter,
152211 );
153- _cachedEventCountries = _CacheEntry (
212+ final cacheKey = 'eventCountry_${nameFilter ?? 'noFilter' }' ;
213+ _cachedEventCountries[cacheKey] = _CacheEntry (
154214 distinctCountries,
155215 DateTime .now ().add (_cacheDuration),
156216 );
157217 _log.info (
158218 'Successfully fetched and cached ${distinctCountries .length } '
159- 'event countries.' ,
219+ 'event countries for key: $ cacheKey .' ,
160220 );
161221 return distinctCountries;
162222 } catch (e, s) {
163223 _log.severe (
164- 'Failed to fetch distinct event countries via aggregation.' ,
224+ 'Failed to fetch distinct event countries via aggregation '
225+ 'with nameFilter: $nameFilter .' ,
165226 e,
166227 s,
167228 );
@@ -170,25 +231,34 @@ class CountryService {
170231 }
171232
172233 /// Helper method to fetch and cache distinct headquarter countries.
173- Future <List <Country >> _fetchAndCacheHeadquarterCountries () async {
174- _log.finer ('Fetching distinct headquarter countries via aggregation.' );
234+ ///
235+ /// - [nameFilter] : An optional map containing a regex filter for the country name.
236+ Future <List <Country >> _fetchAndCacheHeadquarterCountries ({
237+ Map <String , dynamic >? nameFilter,
238+ }) async {
239+ _log.finer (
240+ 'Fetching distinct headquarter countries via aggregation with nameFilter: $nameFilter .' ,
241+ );
175242 try {
176243 final distinctCountries = await _getDistinctCountriesFromAggregation (
177244 repository: _sourceRepository,
178245 fieldName: 'headquarters' ,
246+ nameFilter: nameFilter,
179247 );
180- _cachedHeadquarterCountries = _CacheEntry (
248+ final cacheKey = 'headquarters_${nameFilter ?? 'noFilter' }' ;
249+ _cachedHeadquarterCountries[cacheKey] = _CacheEntry (
181250 distinctCountries,
182251 DateTime .now ().add (_cacheDuration),
183252 );
184253 _log.info (
185254 'Successfully fetched and cached ${distinctCountries .length } '
186- 'headquarter countries.' ,
255+ 'headquarter countries for key: $ cacheKey .' ,
187256 );
188257 return distinctCountries;
189258 } catch (e, s) {
190259 _log.severe (
191- 'Failed to fetch distinct headquarter countries via aggregation.' ,
260+ 'Failed to fetch distinct headquarter countries via aggregation '
261+ 'with nameFilter: $nameFilter .' ,
192262 e,
193263 s,
194264 );
@@ -202,29 +272,40 @@ class CountryService {
202272 /// - [repository] : The [DataRepository] to perform the aggregation on.
203273 /// - [fieldName] : The name of the field within the documents that contains
204274 /// the country object (e.g., 'eventCountry', 'headquarters').
275+ /// - [nameFilter] : An optional map containing a regex filter for the country name.
205276 ///
206277 /// Throws [OperationFailedException] for internal errors during data fetch.
207- Future <List <Country >> _getDistinctCountriesFromAggregation <T extends FeedItem >({
278+ Future <List <Country >>
279+ _getDistinctCountriesFromAggregation <T extends FeedItem >({
208280 required DataRepository <T > repository,
209281 required String fieldName,
282+ Map <String , dynamic >? nameFilter,
210283 }) async {
211- _log.finer ('Fetching distinct countries for field "$fieldName " via aggregation.' );
284+ _log.finer (
285+ 'Fetching distinct countries for field "$fieldName " via aggregation '
286+ 'with nameFilter: $nameFilter .' ,
287+ );
212288 try {
213- final pipeline = [
214- {
215- r'$match' : {
216- 'status' : ContentStatus .active.name,
217- '$fieldName .id' : {r'$exists' : true },
218- },
219- },
220- {
221- r'$group' : {
289+ final matchStage = < String , Object > {
290+ 'status' : ContentStatus .active.name,
291+ '$fieldName .id' : < String , Object > {r'$exists' : true },
292+ };
293+ 294+ // Add name filter if provided
295+ if (nameFilter != null && nameFilter.isNotEmpty) {
296+ matchStage['$fieldName .name' ] = nameFilter;
297+ }
298+ 299+ final pipeline = < Map <String , Object >> [
300+ < String , Object > {r'$match' : matchStage},
301+ < String , Object > {
302+ r'$group' : < String , Object > {
222303 '_id' : '\$ $fieldName .id' ,
223- 'country' : {r'$first' : '\$ $fieldName ' },
304+ 'country' : < String , Object > {r'$first' : '\$ $fieldName ' },
224305 },
225306 },
226- {
227- r'$replaceRoot' : {'newRoot' : r'$country' },
307+ < String , Object > {
308+ r'$replaceRoot' : < String , Object > {'newRoot' : r'$country' },
228309 },
229310 ];
230311
@@ -238,12 +319,13 @@ class CountryService {
238319
239320 _log.info (
240321 'Successfully fetched ${distinctCountries .length } distinct countries '
241- 'for field "$fieldName ".' ,
322+ 'for field "$fieldName " with nameFilter: $ nameFilter .' ,
242323 );
243324 return distinctCountries;
244325 } catch (e, s) {
245326 _log.severe (
246- 'Failed to fetch distinct countries for field "$fieldName ".' ,
327+ 'Failed to fetch distinct countries for field "$fieldName " '
328+ 'with nameFilter: $nameFilter .' ,
247329 e,
248330 s,
249331 );
0 commit comments