Skip to content

Commit c15f78f

Browse files
committed
feat(middlewares): implement data fetch middleware
- Add dataFetchMiddleware to centralize item fetching logic - Implement item fetching from various data repositories based on model name - Handle cases where item is not found or model type is unsupported - Integrate logging for better visibility into fetch operations
1 parent c13a2ad commit c15f78f

File tree

1 file changed

+144
-0
lines changed

1 file changed

+144
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import 'package:core/core.dart';
2+
import 'package:dart_frog/dart_frog.dart';
3+
import 'package:data_repository/data_repository.dart';
4+
import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/ownership_check_middleware.dart';
5+
import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart';
6+
import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart';
7+
import 'package:logging/logging.dart';
8+
9+
final _log = Logger('DataFetchMiddleware');
10+
11+
/// Middleware to fetch a data item by its ID and provide it to the context.
12+
///
13+
/// This middleware is responsible for:
14+
/// 1. Reading the `modelName` and item `id` from the context.
15+
/// 2. Calling the appropriate data repository to fetch the item.
16+
/// 3. If the item is found, providing it to the downstream context wrapped in a
17+
/// [FetchedItem] for type safety.
18+
/// 4. If the item is not found, throwing a [NotFoundException] to halt the
19+
/// request pipeline early.
20+
///
21+
/// This centralizes the item fetching logic for all item-specific routes,
22+
/// ensuring that subsequent middleware (like ownership checks) and the final
23+
/// route handler can safely assume the item exists in the context.
24+
Middleware dataFetchMiddleware() {
25+
return (handler) {
26+
return (context) async {
27+
final modelName = context.read<String>();
28+
final id = context.request.uri.pathSegments.last;
29+
30+
_log.info('Fetching item for model "$modelName", id "$id".');
31+
32+
final item = await _fetchItem(context, modelName, id);
33+
34+
if (item == null) {
35+
_log.warning(
36+
'Item not found for model "$modelName", id "$id".',
37+
);
38+
throw NotFoundException(
39+
'The requested item of type "$modelName" with id "$id" was not found.',
40+
);
41+
}
42+
43+
_log.finer('Item found. Providing to context.');
44+
final updatedContext = context.provide<FetchedItem<dynamic>>(
45+
() => FetchedItem(item),
46+
);
47+
48+
return handler(updatedContext);
49+
};
50+
};
51+
}
52+
53+
/// Helper function to fetch an item from the correct repository based on the
54+
/// model name.
55+
///
56+
/// This function contains the switch statement that maps a `modelName` string
57+
/// to a specific `DataRepository` call.
58+
///
59+
/// Throws [OperationFailedException] for unsupported model types.
60+
Future<dynamic> _fetchItem(
61+
RequestContext context,
62+
String modelName,
63+
String id,
64+
) async {
65+
// The `userId` is not needed here because this middleware's purpose is to
66+
// fetch the item regardless of ownership. Ownership is checked in a
67+
// subsequent middleware. We pass `null` for `userId` to ensure we are
68+
// performing a global lookup for the item.
69+
const String? userId = null;
70+
71+
try {
72+
switch (modelName) {
73+
case 'headline':
74+
return await context.read<DataRepository<Headline>>().read(
75+
id: id,
76+
userId: userId,
77+
);
78+
case 'topic':
79+
return await context
80+
.read<DataRepository<Topic>>()
81+
.read(id: id, userId: userId);
82+
case 'source':
83+
return await context.read<DataRepository<Source>>().read(
84+
id: id,
85+
userId: userId,
86+
);
87+
case 'country':
88+
return await context.read<DataRepository<Country>>().read(
89+
id: id,
90+
userId: userId,
91+
);
92+
case 'language':
93+
return await context.read<DataRepository<Language>>().read(
94+
id: id,
95+
userId: userId,
96+
);
97+
case 'user':
98+
return await context
99+
.read<DataRepository<User>>()
100+
.read(id: id, userId: userId);
101+
case 'user_app_settings':
102+
return await context.read<DataRepository<UserAppSettings>>().read(
103+
id: id,
104+
userId: userId,
105+
);
106+
case 'user_content_preferences':
107+
return await context
108+
.read<DataRepository<UserContentPreferences>>()
109+
.read(
110+
id: id,
111+
userId: userId,
112+
);
113+
case 'remote_config':
114+
return await context.read<DataRepository<RemoteConfig>>().read(
115+
id: id,
116+
userId: userId,
117+
);
118+
case 'dashboard_summary':
119+
// This is a special case that doesn't use a standard repository.
120+
return await context.read<DashboardSummaryService>().getSummary();
121+
default:
122+
_log.warning('Unsupported model type "$modelName" for fetch operation.');
123+
throw OperationFailedException(
124+
'Unsupported model type "$modelName" for fetch operation.',
125+
);
126+
}
127+
} on NotFoundException {
128+
// The repository will throw this if the item doesn't exist.
129+
// We return null to let the main middleware handler throw a more
130+
// detailed exception.
131+
return null;
132+
} catch (e, s) {
133+
_log.severe(
134+
'Unhandled exception in _fetchItem for model "$modelName", id "$id".',
135+
e,
136+
s,
137+
);
138+
// Re-throw as a standard exception type that the main error handler
139+
// can process into a 500 error, while preserving the original cause.
140+
throw OperationFailedException(
141+
'An internal error occurred while fetching the item: $e',
142+
);
143+
}
144+
}

0 commit comments

Comments
 (0)