Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions lib/src/registry/data_operation_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,26 @@ class DataOperationRegistry {
pagination: p,
),
'country': (c, uid, f, s, p) async {
// For 'country' model, delegate to CountryService for specialized filtering.
// The CountryService handles the 'usage' filter and returns a List<Country>.
// We then wrap this list in a PaginatedResponse for consistency with
// the generic API response structure.
final countryService = c.read<CountryService>();
final countries = await countryService.getCountries(f);
return PaginatedResponse<Country>(
items: countries,
cursor: null, // No cursor for this type of filtered list
hasMore: false, // No more items as it's a complete filtered set
);
final usage = f?['usage'] as String?;
if (usage != null && usage.isNotEmpty) {
// For 'country' model with 'usage' filter, delegate to CountryService.
// Sorting and pagination are not supported for this specialized query.
final countryService = c.read<CountryService>();
final countries = await countryService.getCountries(f);
return PaginatedResponse<Country>(
items: countries,
cursor: null, // No cursor for this type of filtered list
hasMore: false, // No more items as it's a complete filtered set
);
} else {
// For standard requests, use the repository which supports pagination/sorting.
return c.read<DataRepository<Country>>().readAll(
userId: uid,
filter: f,
sort: s,
pagination: p,
);
}
},
'language': (c, uid, f, s, p) => c
.read<DataRepository<Language>>()
Expand Down
138 changes: 81 additions & 57 deletions lib/src/services/country_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,30 @@ import 'package:core/core.dart';
import 'package:data_repository/data_repository.dart';
import 'package:logging/logging.dart';

/// {@template _cache_entry}
/// A simple class to hold cached data along with its expiration time.
/// {@endtemplate}
class _CacheEntry<T> {
/// {@macro _cache_entry}
const _CacheEntry(this.data, this.expiry);

/// The cached data.
final T data;

/// The time at which the cached data expires.
final DateTime expiry;

/// Checks if the cache entry is still valid (not expired).
bool isValid() => DateTime.now().isBefore(expiry);
}

/// {@template country_service}
/// A service responsible for retrieving country data, including specialized
/// lists like countries associated with headlines or sources.
///
/// This service leverages database aggregation for efficient data retrieval
/// and includes basic in-memory caching to optimize performance for frequently
/// requested lists.
/// and includes time-based in-memory caching to optimize performance for
/// frequently requested lists.
/// {@endtemplate}
class CountryService {
/// {@macro country_service}
Expand All @@ -27,11 +44,12 @@ class CountryService {
final DataRepository<Source> _sourceRepository;
final Logger _log;

// In-memory caches for frequently accessed lists.
// These should be cleared periodically in a real-world application
// or invalidated upon data changes. For this scope, simple caching is used.
List<Country>? _cachedEventCountries;
List<Country>? _cachedHeadquarterCountries;
// Cache duration for aggregated country lists (e.g., 1 hour).
static const Duration _cacheDuration = Duration(hours: 1);

// In-memory caches for frequently accessed lists with time-based invalidation.
_CacheEntry<List<Country>>? _cachedEventCountries;
_CacheEntry<List<Country>>? _cachedHeadquarterCountries;

/// Retrieves a list of countries based on the provided filter.
///
Expand Down Expand Up @@ -91,49 +109,26 @@ class CountryService {
/// Uses MongoDB aggregation to efficiently get distinct country IDs
/// and then fetches the full Country objects. Results are cached.
Future<List<Country>> _getEventCountries() async {
if (_cachedEventCountries != null) {
if (_cachedEventCountries != null && _cachedEventCountries!.isValid()) {
_log.finer('Returning cached event countries.');
return _cachedEventCountries!;
return _cachedEventCountries!.data;
}

_log.finer('Fetching distinct event countries via aggregation.');
try {
final pipeline = [
{
r'$match': {
'status': ContentStatus.active.name,
'eventCountry.id': {r'$exists': true},
},
},
{
r'$group': {
'_id': r'$eventCountry.id',
'country': {r'$first': r'$eventCountry'},
},
},
{
r'$replaceRoot': {'newRoot': r'$country'},
},
];

final distinctCountriesJson = await _headlineRepository.aggregate(
pipeline: pipeline,
);

final distinctCountries = distinctCountriesJson
.map(Country.fromJson)
.toList();

_cachedEventCountries = distinctCountries;
_log.info(
'Successfully fetched and cached ${distinctCountries.length} '
'event countries.',
);
return distinctCountries;
} catch (e, s) {
_log.severe('Failed to fetch event countries via aggregation.', e, s);
throw OperationFailedException('Failed to retrieve event countries: $e');
}
final distinctCountries = await _getDistinctCountriesFromAggregation(
repository: _headlineRepository,
fieldName: 'eventCountry',
);

_cachedEventCountries = _CacheEntry(
distinctCountries,
DateTime.now().add(_cacheDuration),
);
_log.info(
'Successfully fetched and cached ${distinctCountries.length} '
'event countries.',
);
return distinctCountries;
}

/// Fetches a distinct list of countries that are referenced as
Expand All @@ -142,53 +137,82 @@ class CountryService {
/// Uses MongoDB aggregation to efficiently get distinct country IDs
/// and then fetches the full Country objects. Results are cached.
Future<List<Country>> _getHeadquarterCountries() async {
if (_cachedHeadquarterCountries != null) {
if (_cachedHeadquarterCountries != null &&
_cachedHeadquarterCountries!.isValid()) {
_log.finer('Returning cached headquarter countries.');
return _cachedHeadquarterCountries!;
return _cachedHeadquarterCountries!.data;
}

_log.finer('Fetching distinct headquarter countries via aggregation.');
final distinctCountries = await _getDistinctCountriesFromAggregation(
repository: _sourceRepository,
fieldName: 'headquarters',
);

_cachedHeadquarterCountries = _CacheEntry(
distinctCountries,
DateTime.now().add(_cacheDuration),
);
_log.info(
'Successfully fetched and cached ${distinctCountries.length} '
'headquarter countries.',
);
return distinctCountries;
}

/// Helper method to fetch a distinct list of countries from a given
/// repository and field name using MongoDB aggregation.
///
/// - [repository]: The [DataRepository] to perform the aggregation on.
/// - [fieldName]: The name of the field within the documents that contains
/// the country object (e.g., 'eventCountry', 'headquarters').
///
/// Throws [OperationFailedException] for internal errors during data fetch.
Future<List<Country>> _getDistinctCountriesFromAggregation({
required DataRepository<dynamic> repository,
required String fieldName,
}) async {
_log.finer('Fetching distinct countries for field "$fieldName" via aggregation.');
try {
final pipeline = [
{
r'$match': {
'status': ContentStatus.active.name,
'headquarters.id': {r'$exists': true},
'$fieldName.id': {r'$exists': true},
},
},
{
r'$group': {
'_id': r'$headquarters.id',
'country': {r'$first': r'$headquarters'},
'_id': '\$$fieldName.id',
'country': {r'$first': '\$$fieldName'},
},
},
{
r'$replaceRoot': {'newRoot': r'$country'},
},
];

final distinctCountriesJson = await _sourceRepository.aggregate(
final distinctCountriesJson = await repository.aggregate(
pipeline: pipeline,
);

final distinctCountries = distinctCountriesJson
.map(Country.fromJson)
.toList();

_cachedHeadquarterCountries = distinctCountries;
_log.info(
'Successfully fetched and cached ${distinctCountries.length} '
'headquarter countries.',
'Successfully fetched ${distinctCountries.length} distinct countries '
'for field "$fieldName".',
);
return distinctCountries;
} catch (e, s) {
_log.severe(
'Failed to fetch headquarter countries via aggregation.',
'Failed to fetch distinct countries for field "$fieldName".',
e,
s,
);
throw OperationFailedException(
'Failed to retrieve headquarter countries: $e',
'Failed to retrieve distinct countries for field "$fieldName": $e',
);
}
}
Expand Down
Loading