Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 53e0df1

Browse files
authored
Merge pull request #50 from flutter-news-app-full-source-code/integrate-name-based-filtering-for-the-country-model
Integrate name based filtering for the country model
2 parents fb98c6e + 228cca9 commit 53e0df1

File tree

3 files changed

+162
-71
lines changed

3 files changed

+162
-71
lines changed

‎lib/src/registry/data_operation_registry.dart‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,12 @@ class DataOperationRegistry {
131131
),
132132
'country': (c, uid, f, s, p) async {
133133
final usage = f?['usage'] as String?;
134-
if (usage != null && usage.isNotEmpty) {
135-
// For 'country' model with 'usage' filter, delegate to CountryService.
136-
// Sorting and pagination are not supported for this specialized query.
134+
final name = f?['name'] as String?;
135+
136+
// If either 'usage' or 'name' filter is present, delegate to CountryService.
137+
// Sorting and pagination are handled by CountryService for these specialized queries.
138+
if ((usage != null && usage.isNotEmpty) ||
139+
(name != null && name.isNotEmpty)) {
137140
final countryService = c.read<CountryService>();
138141
final countries = await countryService.getCountries(f);
139142
return PaginatedResponse<Country>(
@@ -142,13 +145,14 @@ class DataOperationRegistry {
142145
hasMore: false, // No more items as it's a complete filtered set
143146
);
144147
} else {
145-
// For standard requests, use the repository which supports pagination/sorting.
148+
// For standard requests without specialized filters, use the repository
149+
// which supports pagination/sorting.
146150
return c.read<DataRepository<Country>>().readAll(
147-
userId: uid,
148-
filter: f,
149-
sort: s,
150-
pagination: p,
151-
);
151+
userId: uid,
152+
filter: f,
153+
sort: s,
154+
pagination: p,
155+
);
152156
}
153157
},
154158
'language': (c, uid, f, s, p) => c

‎lib/src/services/country_service.dart‎

Lines changed: 144 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
finalMap<String, Future<List<Country>>> _eventCountriesFutures = {};
57+
finalMap<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
);

‎lib/src/services/database_seeding_service.dart‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ class DatabaseSeedingService {
118118
.collection('sources')
119119
.createIndex(keys: {'name': 'text'}, name: 'sources_text_index');
120120

121+
// Index for searching countries by name (case-insensitive friendly)
122+
await _db
123+
.collection('countries')
124+
.createIndex(keys: {'name': 1}, name: 'countries_name_index');
125+
121126
// Indexes for country aggregation queries
122127
await _db
123128
.collection('headlines')

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /