Skip to content

Commit 56feee3

Browse files
authored
Merge pull request #41 from flutter-news-app-full-source-code/enhance-data-route
Enhance data route
2 parents 62f0f50 + aae6957 commit 56feee3

File tree

5 files changed

+315
-291
lines changed

5 files changed

+315
-291
lines changed

lib/src/middlewares/data_fetch_middleware.dart

Lines changed: 16 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import 'package:core/core.dart';
22
import 'package:dart_frog/dart_frog.dart';
3-
import 'package:data_repository/data_repository.dart';
43
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/services/dashboard_summary_service.dart';
4+
import 'package:flutter_news_app_api_server_full_source_code/src/registry/data_operation_registry.dart';
65
import 'package:logging/logging.dart';
76

87
final _log = Logger('DataFetchMiddleware');
@@ -31,9 +30,7 @@ Middleware dataFetchMiddleware() {
3130
final item = await _fetchItem(context, modelName, id);
3231

3332
if (item == null) {
34-
_log.warning(
35-
'Item not found for model "$modelName", id "$id".',
36-
);
33+
_log.warning('Item not found for model "$modelName", id "$id".');
3734
throw NotFoundException(
3835
'The requested item of type "$modelName" with id "$id" was not found.',
3936
);
@@ -50,79 +47,30 @@ Middleware dataFetchMiddleware() {
5047
}
5148

5249
/// Helper function to fetch an item from the correct repository based on the
53-
/// model name.
50+
/// model name by using the [DataOperationRegistry].
5451
///
55-
/// This function contains the switch statement that maps a `modelName` string
56-
/// to a specific `DataRepository` call.
52+
/// This function looks up the appropriate fetcher function from the registry
53+
/// and invokes it. This avoids a large `switch` statement and makes the
54+
/// system easily extensible.
5755
///
5856
/// Throws [OperationFailedException] for unsupported model types.
5957
Future<dynamic> _fetchItem(
6058
RequestContext context,
6159
String modelName,
6260
String id,
6361
) async {
64-
// The `userId` is not needed here because this middleware's purpose is to
65-
// fetch the item regardless of ownership. Ownership is checked in a
66-
// subsequent middleware. We pass `null` for `userId` to ensure we are
67-
// performing a global lookup for the item.
68-
const String? userId = null;
69-
7062
try {
71-
switch (modelName) {
72-
case 'headline':
73-
return await context.read<DataRepository<Headline>>().read(
74-
id: id,
75-
userId: userId,
76-
);
77-
case 'topic':
78-
return await context
79-
.read<DataRepository<Topic>>()
80-
.read(id: id, userId: userId);
81-
case 'source':
82-
return await context.read<DataRepository<Source>>().read(
83-
id: id,
84-
userId: userId,
85-
);
86-
case 'country':
87-
return await context.read<DataRepository<Country>>().read(
88-
id: id,
89-
userId: userId,
90-
);
91-
case 'language':
92-
return await context.read<DataRepository<Language>>().read(
93-
id: id,
94-
userId: userId,
95-
);
96-
case 'user':
97-
return await context
98-
.read<DataRepository<User>>()
99-
.read(id: id, userId: userId);
100-
case 'user_app_settings':
101-
return await context.read<DataRepository<UserAppSettings>>().read(
102-
id: id,
103-
userId: userId,
104-
);
105-
case 'user_content_preferences':
106-
return await context
107-
.read<DataRepository<UserContentPreferences>>()
108-
.read(
109-
id: id,
110-
userId: userId,
111-
);
112-
case 'remote_config':
113-
return await context.read<DataRepository<RemoteConfig>>().read(
114-
id: id,
115-
userId: userId,
116-
);
117-
case 'dashboard_summary':
118-
// This is a special case that doesn't use a standard repository.
119-
return await context.read<DashboardSummaryService>().getSummary();
120-
default:
121-
_log.warning('Unsupported model type "$modelName" for fetch operation.');
122-
throw OperationFailedException(
123-
'Unsupported model type "$modelName" for fetch operation.',
124-
);
63+
final registry = context.read<DataOperationRegistry>();
64+
final fetcher = registry.itemFetchers[modelName];
65+
66+
if (fetcher == null) {
67+
_log.warning('Unsupported model type "$modelName" for fetch operation.');
68+
throw OperationFailedException(
69+
'Unsupported model type "$modelName" for fetch operation.',
70+
);
12571
}
72+
73+
return await fetcher(context, id);
12674
} on NotFoundException {
12775
// The repository will throw this if the item doesn't exist.
12876
// We return null to let the main middleware handler throw a more
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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/services/dashboard_summary_service.dart';
6+
7+
// --- Typedefs for Data Operations ---
8+
9+
/// A function that fetches a single item by its ID.
10+
typedef ItemFetcher =
11+
Future<dynamic> Function(RequestContext context, String id);
12+
13+
/// A function that fetches a paginated list of items.
14+
typedef AllItemsReader =
15+
Future<PaginatedResponse<dynamic>> Function(
16+
RequestContext context,
17+
String? userId,
18+
Map<String, dynamic>? filter,
19+
List<SortOption>? sort,
20+
PaginationOptions? pagination,
21+
);
22+
23+
/// A function that creates a new item.
24+
typedef ItemCreator =
25+
Future<dynamic> Function(
26+
RequestContext context,
27+
dynamic item,
28+
String? userId,
29+
);
30+
31+
/// A function that updates an existing item.
32+
typedef ItemUpdater =
33+
Future<dynamic> Function(
34+
RequestContext context,
35+
String id,
36+
dynamic item,
37+
String? userId,
38+
);
39+
40+
/// A function that deletes an item by its ID.
41+
typedef ItemDeleter =
42+
Future<void> Function(RequestContext context, String id, String? userId);
43+
44+
/// {@template data_operation_registry}
45+
/// A centralized registry for all data handling functions (CRUD operations).
46+
///
47+
/// This class uses a map-based strategy to associate model names (strings)
48+
/// with their corresponding data operation functions. This approach avoids
49+
/// large, repetitive `switch` statements in the route handlers, making the
50+
/// code more maintainable, scalable, and easier to read.
51+
///
52+
/// By centralizing these mappings, we create a single source of truth for how
53+
/// data operations are performed for each model, improving consistency across
54+
/// the API.
55+
/// {@endtemplate}
56+
class DataOperationRegistry {
57+
/// {@macro data_operation_registry}
58+
DataOperationRegistry() {
59+
_registerOperations();
60+
}
61+
62+
// --- Private Maps to Hold Operation Functions ---
63+
64+
final Map<String, ItemFetcher> _itemFetchers = {};
65+
final Map<String, AllItemsReader> _allItemsReaders = {};
66+
final Map<String, ItemCreator> _itemCreators = {};
67+
final Map<String, ItemUpdater> _itemUpdaters = {};
68+
final Map<String, ItemDeleter> _itemDeleters = {};
69+
70+
// --- Public Getters to Access Operation Maps ---
71+
72+
/// Provides access to the map of item fetcher functions.
73+
Map<String, ItemFetcher> get itemFetchers => _itemFetchers;
74+
75+
/// Provides access to the map of collection reader functions.
76+
Map<String, AllItemsReader> get allItemsReaders => _allItemsReaders;
77+
78+
/// Provides access to the map of item creator functions.
79+
Map<String, ItemCreator> get itemCreators => _itemCreators;
80+
81+
/// Provides access to the map of item updater functions.
82+
Map<String, ItemUpdater> get itemUpdaters => _itemUpdaters;
83+
84+
/// Provides access to the map of item deleter functions.
85+
Map<String, ItemDeleter> get itemDeleters => _itemDeleters;
86+
87+
/// Populates the operation maps with their respective functions.
88+
void _registerOperations() {
89+
// --- Register Item Fetchers ---
90+
_itemFetchers.addAll({
91+
'headline': (c, id) =>
92+
c.read<DataRepository<Headline>>().read(id: id, userId: null),
93+
'topic': (c, id) =>
94+
c.read<DataRepository<Topic>>().read(id: id, userId: null),
95+
'source': (c, id) =>
96+
c.read<DataRepository<Source>>().read(id: id, userId: null),
97+
'country': (c, id) =>
98+
c.read<DataRepository<Country>>().read(id: id, userId: null),
99+
'language': (c, id) =>
100+
c.read<DataRepository<Language>>().read(id: id, userId: null),
101+
'user': (c, id) =>
102+
c.read<DataRepository<User>>().read(id: id, userId: null),
103+
'user_app_settings': (c, id) =>
104+
c.read<DataRepository<UserAppSettings>>().read(id: id, userId: null),
105+
'user_content_preferences': (c, id) => c
106+
.read<DataRepository<UserContentPreferences>>()
107+
.read(id: id, userId: null),
108+
'remote_config': (c, id) =>
109+
c.read<DataRepository<RemoteConfig>>().read(id: id, userId: null),
110+
'dashboard_summary': (c, id) =>
111+
c.read<DashboardSummaryService>().getSummary(),
112+
});
113+
114+
// --- Register "Read All" Readers ---
115+
_allItemsReaders.addAll({
116+
'headline': (c, uid, f, s, p) => c
117+
.read<DataRepository<Headline>>()
118+
.readAll(userId: uid, filter: f, sort: s, pagination: p),
119+
'topic': (c, uid, f, s, p) => c.read<DataRepository<Topic>>().readAll(
120+
userId: uid,
121+
filter: f,
122+
sort: s,
123+
pagination: p,
124+
),
125+
'source': (c, uid, f, s, p) => c.read<DataRepository<Source>>().readAll(
126+
userId: uid,
127+
filter: f,
128+
sort: s,
129+
pagination: p,
130+
),
131+
'country': (c, uid, f, s, p) => c.read<DataRepository<Country>>().readAll(
132+
userId: uid,
133+
filter: f,
134+
sort: s,
135+
pagination: p,
136+
),
137+
'language': (c, uid, f, s, p) => c
138+
.read<DataRepository<Language>>()
139+
.readAll(userId: uid, filter: f, sort: s, pagination: p),
140+
'user': (c, uid, f, s, p) => c.read<DataRepository<User>>().readAll(
141+
userId: uid,
142+
filter: f,
143+
sort: s,
144+
pagination: p,
145+
),
146+
});
147+
148+
// --- Register Item Creators ---
149+
_itemCreators.addAll({
150+
'headline': (c, item, uid) => c.read<DataRepository<Headline>>().create(
151+
item: item as Headline,
152+
userId: uid,
153+
),
154+
'topic': (c, item, uid) => c.read<DataRepository<Topic>>().create(
155+
item: item as Topic,
156+
userId: uid,
157+
),
158+
'source': (c, item, uid) => c.read<DataRepository<Source>>().create(
159+
item: item as Source,
160+
userId: uid,
161+
),
162+
'country': (c, item, uid) => c.read<DataRepository<Country>>().create(
163+
item: item as Country,
164+
userId: uid,
165+
),
166+
'language': (c, item, uid) => c.read<DataRepository<Language>>().create(
167+
item: item as Language,
168+
userId: uid,
169+
),
170+
'remote_config': (c, item, uid) => c
171+
.read<DataRepository<RemoteConfig>>()
172+
.create(item: item as RemoteConfig, userId: uid),
173+
});
174+
175+
// --- Register Item Updaters ---
176+
_itemUpdaters.addAll({
177+
'headline': (c, id, item, uid) => c
178+
.read<DataRepository<Headline>>()
179+
.update(id: id, item: item as Headline, userId: uid),
180+
'topic': (c, id, item, uid) => c.read<DataRepository<Topic>>().update(
181+
id: id,
182+
item: item as Topic,
183+
userId: uid,
184+
),
185+
'source': (c, id, item, uid) => c.read<DataRepository<Source>>().update(
186+
id: id,
187+
item: item as Source,
188+
userId: uid,
189+
),
190+
'country': (c, id, item, uid) => c.read<DataRepository<Country>>().update(
191+
id: id,
192+
item: item as Country,
193+
userId: uid,
194+
),
195+
'language': (c, id, item, uid) => c
196+
.read<DataRepository<Language>>()
197+
.update(id: id, item: item as Language, userId: uid),
198+
'user': (c, id, item, uid) {
199+
final repo = c.read<DataRepository<User>>();
200+
final existingUser = c.read<FetchedItem<dynamic>>().data as User;
201+
final updatedUser = existingUser.copyWith(
202+
feedActionStatus: (item as User).feedActionStatus,
203+
);
204+
return repo.update(id: id, item: updatedUser, userId: uid);
205+
},
206+
'user_app_settings': (c, id, item, uid) => c
207+
.read<DataRepository<UserAppSettings>>()
208+
.update(id: id, item: item as UserAppSettings, userId: uid),
209+
'user_content_preferences': (c, id, item, uid) => c
210+
.read<DataRepository<UserContentPreferences>>()
211+
.update(id: id, item: item as UserContentPreferences, userId: uid),
212+
'remote_config': (c, id, item, uid) => c
213+
.read<DataRepository<RemoteConfig>>()
214+
.update(id: id, item: item as RemoteConfig, userId: uid),
215+
});
216+
217+
// --- Register Item Deleters ---
218+
_itemDeleters.addAll({
219+
'headline': (c, id, uid) =>
220+
c.read<DataRepository<Headline>>().delete(id: id, userId: uid),
221+
'topic': (c, id, uid) =>
222+
c.read<DataRepository<Topic>>().delete(id: id, userId: uid),
223+
'source': (c, id, uid) =>
224+
c.read<DataRepository<Source>>().delete(id: id, userId: uid),
225+
'country': (c, id, uid) =>
226+
c.read<DataRepository<Country>>().delete(id: id, userId: uid),
227+
'language': (c, id, uid) =>
228+
c.read<DataRepository<Language>>().delete(id: id, userId: uid),
229+
'user': (c, id, uid) =>
230+
c.read<DataRepository<User>>().delete(id: id, userId: uid),
231+
'user_app_settings': (c, id, uid) =>
232+
c.read<DataRepository<UserAppSettings>>().delete(id: id, userId: uid),
233+
'user_content_preferences': (c, id, uid) => c
234+
.read<DataRepository<UserContentPreferences>>()
235+
.delete(id: id, userId: uid),
236+
'remote_config': (c, id, uid) =>
237+
c.read<DataRepository<RemoteConfig>>().delete(id: id, userId: uid),
238+
});
239+
}
240+
}

routes/_middleware.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter_news_app_api_server_full_source_code/src/config/app_depe
66
import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/error_handler.dart';
77
import 'package:flutter_news_app_api_server_full_source_code/src/models/request_id.dart';
88
import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart';
9+
import 'package:flutter_news_app_api_server_full_source_code/src/registry/data_operation_registry.dart';
910
import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart';
1011
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart';
1112
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_token_service.dart';
@@ -84,6 +85,9 @@ Handler middleware(Handler handler) {
8485
// 2. Provide all dependencies to the inner handler.
8586
final deps = AppDependencies.instance;
8687
return handler
88+
.use(
89+
provider<DataOperationRegistry>((_) => DataOperationRegistry()),
90+
)
8791
.use(provider<ModelRegistryMap>((_) => modelRegistry))
8892
.use(
8993
provider<DataRepository<Headline>>(

0 commit comments

Comments
 (0)