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 5d559e2

Browse files
authored
Merge pull request #14 from headlines-toolkit/refactor_sync_with_new_models_api
Refactor sync with new models api
2 parents 5e9f921 + 228396e commit 5d559e2

25 files changed

+887
-785
lines changed

‎README.md‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,37 @@ management dashboard](https://github.com/headlines-toolkit/ht-dashboard).
1717
## ✨ Key Capabilities
1818

1919
* 🔒 **Flexible & Secure Authentication:** Provide seamless user access with
20-
a unified system supporting passwordless sign-in, anonymous guest
21-
accounts, and a secure, context-aware login flow for privileged dashboard
22-
users (e.g., 'admin', 'publisher').
20+
a unified system supporting passwordless email sign-in, anonymous guest
21+
accounts, and a secure, role-aware login flow for privileged dashboard
22+
users.
2323

24-
* ⚡️ **Flexible Role-Based Access Control (RBAC):** Implement granular
25-
permissions with a flexible, multi-role system. Assign multiple roles to
26-
users (e.g., 'admin', 'publisher', 'premium_user') to precisely control
27-
access to different API features and data management capabilities.
24+
* ⚡️ **Granular Role-Based Access Control (RBAC):** Implement precise
25+
permissions with a dual-role system (`appRole` for application features,
26+
`dashboardRole` for admin functions) to control access to API features
27+
and data management capabilities.
2828

2929
* ⚙️ **Synchronized App Settings:** Ensure a consistent and personalized user
3030
experience across devices by effortlessly syncing application preferences
3131
like theme, language, font styles, and more.
3232

3333
* 👤 **Personalized User Preferences:** Enable richer user interactions by
34-
managing and syncing user-specific data such as saved headlines, followed sources, or other personalized content tailored to individual users.
34+
managing and syncing user-specific data such as saved headlines, followed
35+
sources, and followed topics tailored to individual users.
3536

3637
* 💾 **Robust Data Management:** Securely manage core news data (headlines,
37-
categories, sources) through a well-structured API that supports flexible
38+
topics, sources) through a well-structured API that supports flexible
39+
querying and sorting for dynamic content presentation.
40+
41+
* 🌐 **Dynamic Remote Configuration:** Centrally manage application
42+
behavior—including ad frequency, feature flags, and maintenance status—without
43+
requiring a client-side update.
44+
45+
* 💾 **Robust Data Management:** Securely manage core news data (headlines,
46+
topics, sources) through a well-structured API that supports flexible
3847
querying and sorting for dynamic content presentation.
3948

4049
* 📊 **Dynamic Dashboard Summary:** Access real-time, aggregated metrics on
41-
key data points like total headlines, categories, and sources, providing
50+
key data points like total headlines, topics, and sources, providing
4251
an at-a-glance overview for administrative dashboards.
4352

4453
* 🔧 **Solid Technical Foundation:** Built with Dart and the high-performance

‎lib/src/config/app_dependencies.dart‎

Lines changed: 100 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,54 @@ class AppDependencies {
3838
final _completer = Completer<void>();
3939

4040
// --- Repositories ---
41+
/// A repository for managing [Headline] data.
4142
late final HtDataRepository<Headline> headlineRepository;
42-
late final HtDataRepository<Category> categoryRepository;
43+
44+
/// A repository for managing [Topic] data.
45+
late final HtDataRepository<Topic> topicRepository;
46+
47+
/// A repository for managing [Source] data.
4348
late final HtDataRepository<Source> sourceRepository;
49+
50+
/// A repository for managing [Country] data.
4451
late final HtDataRepository<Country> countryRepository;
52+
53+
/// A repository for managing [User] data.
4554
late final HtDataRepository<User> userRepository;
55+
56+
/// A repository for managing [UserAppSettings] data.
4657
late final HtDataRepository<UserAppSettings> userAppSettingsRepository;
58+
59+
/// A repository for managing [UserContentPreferences] data.
4760
late final HtDataRepository<UserContentPreferences>
4861
userContentPreferencesRepository;
49-
late final HtDataRepository<AppConfig> appConfigRepository;
62+
63+
/// A repository for managing the global [RemoteConfig] data.
64+
late final HtDataRepository<RemoteConfig> remoteConfigRepository;
5065

5166
// --- Services ---
67+
/// A service for sending emails.
5268
late final HtEmailRepository emailRepository;
69+
70+
/// A service for managing a blacklist of invalidated authentication tokens.
5371
late final TokenBlacklistService tokenBlacklistService;
72+
73+
/// A service for generating and validating authentication tokens.
5474
late final AuthTokenService authTokenService;
75+
76+
/// A service for storing and validating one-time verification codes.
5577
late final VerificationCodeStorageService verificationCodeStorageService;
78+
79+
/// A service that orchestrates authentication logic.
5680
late final AuthService authService;
81+
82+
/// A service for calculating and providing a summary for the dashboard.
5783
late final DashboardSummaryService dashboardSummaryService;
84+
85+
/// A service for checking user permissions.
5886
late final PermissionService permissionService;
87+
88+
/// A service for enforcing limits on user content preferences.
5989
late final UserPreferenceLimitService userPreferenceLimitService;
6090

6191
/// Initializes all application dependencies.
@@ -100,189 +130,107 @@ class AppDependencies {
100130
headlineRepository = _createRepository(
101131
connection,
102132
'headlines',
103-
(json) {
104-
if (json['created_at'] is DateTime) {
105-
json['created_at'] =
106-
(json['created_at'] as DateTime).toIso8601String();
107-
}
108-
if (json['updated_at'] is DateTime) {
109-
json['updated_at'] =
110-
(json['updated_at'] as DateTime).toIso8601String();
111-
}
112-
if (json['published_at'] is DateTime) {
113-
json['published_at'] =
114-
(json['published_at'] as DateTime).toIso8601String();
115-
}
116-
return Headline.fromJson(json);
117-
},
118-
(headline) {
119-
final json = headline.toJson();
120-
// The database expects source_id and category_id, not nested objects.
121-
// We extract the IDs and remove the original objects to match the
122-
// schema.
123-
if (headline.source != null) {
124-
json['source_id'] = headline.source!.id;
125-
}
126-
if (headline.category != null) {
127-
json['category_id'] = headline.category!.id;
128-
}
129-
json.remove('source');
130-
json.remove('category');
131-
return json;
132-
},
133+
// The HtDataPostgresClient returns DateTime objects from TIMESTAMPTZ
134+
// columns. The Headline.fromJson factory expects ISO 8601 strings.
135+
// This handler converts them before deserialization.
136+
(json) => Headline.fromJson(_convertTimestampsToString(json)),
137+
(headline) => headline.toJson()
138+
..['source_id'] = headline.source.id
139+
..['topic_id'] = headline.topic.id
140+
..['event_country_id'] = headline.eventCountry.id
141+
..remove('source')
142+
..remove('topic')
143+
..remove('eventCountry'),
133144
);
134-
categoryRepository = _createRepository(
145+
topicRepository = _createRepository(
135146
connection,
136-
'categories',
137-
(json) {
138-
if (json['created_at'] is DateTime) {
139-
json['created_at'] =
140-
(json['created_at'] as DateTime).toIso8601String();
141-
}
142-
if (json['updated_at'] is DateTime) {
143-
json['updated_at'] =
144-
(json['updated_at'] as DateTime).toIso8601String();
145-
}
146-
return Category.fromJson(json);
147-
},
148-
(c) => c.toJson(),
147+
'topics',
148+
(json) => Topic.fromJson(_convertTimestampsToString(json)),
149+
(topic) => topic.toJson(),
149150
);
150151
sourceRepository = _createRepository(
151152
connection,
152153
'sources',
153-
(json) {
154-
if (json['created_at'] is DateTime) {
155-
json['created_at'] =
156-
(json['created_at'] as DateTime).toIso8601String();
157-
}
158-
if (json['updated_at'] is DateTime) {
159-
json['updated_at'] =
160-
(json['updated_at'] as DateTime).toIso8601String();
161-
}
162-
return Source.fromJson(json);
163-
},
164-
(source) {
165-
final json = source.toJson();
166-
// The database expects headquarters_country_id, not a nested object.
167-
// We extract the ID and remove the original object to match the
168-
// schema.
169-
json['headquarters_country_id'] = source.headquarters?.id;
170-
json.remove('headquarters');
171-
return json;
172-
},
154+
(json) => Source.fromJson(_convertTimestampsToString(json)),
155+
(source) => source.toJson()
156+
..['headquarters_country_id'] = source.headquarters.id
157+
..remove('headquarters'),
173158
);
174159
countryRepository = _createRepository(
175160
connection,
176161
'countries',
177-
(json) {
178-
if (json['created_at'] is DateTime) {
179-
json['created_at'] =
180-
(json['created_at'] as DateTime).toIso8601String();
181-
}
182-
if (json['updated_at'] is DateTime) {
183-
json['updated_at'] =
184-
(json['updated_at'] as DateTime).toIso8601String();
185-
}
186-
return Country.fromJson(json);
187-
},
188-
(c) => c.toJson(),
162+
(json) => Country.fromJson(_convertTimestampsToString(json)),
163+
(country) => country.toJson(),
189164
);
190165
userRepository = _createRepository(
191166
connection,
192167
'users',
193-
(json) {
194-
// The postgres driver returns DateTime objects, but the model's
195-
// fromJson expects ISO 8601 strings. We must convert them first.
196-
if (json['created_at'] is DateTime) {
197-
json['created_at'] = (json['created_at'] as DateTime).toIso8601String();
198-
}
199-
if (json['last_engagement_shown_at'] is DateTime) {
200-
json['last_engagement_shown_at'] =
201-
(json['last_engagement_shown_at'] as DateTime).toIso8601String();
202-
}
203-
return User.fromJson(json);
204-
},
168+
(json) => User.fromJson(_convertTimestampsToString(json)),
205169
(user) {
206-
// The `roles` field is a List<String>, but the database expects a
207-
// JSONB array. We must explicitly encode it.
208170
final json = user.toJson();
209-
json['roles'] = jsonEncode(json['roles']);
171+
// Convert enums to their string names for the database.
172+
json['app_role'] = user.appRole.name;
173+
json['dashboard_role'] = user.dashboardRole.name;
174+
// The `feed_action_status` map must be JSON encoded for the JSONB column.
175+
json['feed_action_status'] = jsonEncode(json['feed_action_status']);
210176
return json;
211177
},
212178
);
213179
userAppSettingsRepository = _createRepository(
214180
connection,
215181
'user_app_settings',
216-
(json) {
217-
// The DB has created_at/updated_at, but the model doesn't.
218-
// Remove them before deserialization to avoid CheckedFromJsonException.
219-
json.remove('created_at');
220-
json.remove('updated_at');
221-
return UserAppSettings.fromJson(json);
222-
},
182+
UserAppSettings.fromJson,
223183
(settings) {
224184
final json = settings.toJson();
225185
// These fields are complex objects and must be JSON encoded for the DB.
226186
json['display_settings'] = jsonEncode(json['display_settings']);
227187
json['feed_preferences'] = jsonEncode(json['feed_preferences']);
228-
json['engagement_shown_counts'] =
229-
jsonEncode(json['engagement_shown_counts']);
230-
json['engagement_last_shown_timestamps'] =
231-
jsonEncode(json['engagement_last_shown_timestamps']);
232188
return json;
233189
},
234190
);
235191
userContentPreferencesRepository = _createRepository(
236192
connection,
237193
'user_content_preferences',
238-
(json) {
239-
// The postgres driver returns DateTime objects, but the model's
240-
// fromJson expects ISO 8601 strings. We must convert them first.
241-
if (json['created_at'] is DateTime) {
242-
json['created_at'] =
243-
(json['created_at'] as DateTime).toIso8601String();
244-
}
245-
if (json['updated_at'] is DateTime) {
246-
json['updated_at'] =
247-
(json['updated_at'] as DateTime).toIso8601String();
248-
}
249-
return UserContentPreferences.fromJson(json);
250-
},
194+
UserContentPreferences.fromJson,
251195
(preferences) {
252196
final json = preferences.toJson();
253-
json['followed_categories'] = jsonEncode(json['followed_categories']);
197+
// These fields are lists of complex objects and must be JSON encoded.
198+
json['followed_topics'] = jsonEncode(json['followed_topics']);
254199
json['followed_sources'] = jsonEncode(json['followed_sources']);
255200
json['followed_countries'] = jsonEncode(json['followed_countries']);
256201
json['saved_headlines'] = jsonEncode(json['saved_headlines']);
257202
return json;
258203
},
259204
);
260-
appConfigRepository = _createRepository(
205+
remoteConfigRepository = _createRepository(
261206
connection,
262-
'app_config',
263-
(json) {
264-
if (json['created_at'] is DateTime) {
265-
json['created_at'] =
266-
(json['created_at'] as DateTime).toIso8601String();
267-
}
268-
if (json['updated_at'] is DateTime) {
269-
json['updated_at'] =
270-
(json['updated_at'] as DateTime).toIso8601String();
271-
}
272-
return AppConfig.fromJson(json);
207+
'remote_config',
208+
(json) => RemoteConfig.fromJson(_convertTimestampsToString(json)),
209+
(config) {
210+
final json = config.toJson();
211+
// All nested config objects must be JSON encoded for JSONB columns.
212+
json['user_preference_limits'] = jsonEncode(
213+
json['user_preference_limits'],
214+
);
215+
json['ad_config'] = jsonEncode(json['ad_config']);
216+
json['account_action_config'] = jsonEncode(
217+
json['account_action_config'],
218+
);
219+
json['app_status'] = jsonEncode(json['app_status']);
220+
return json;
273221
},
274-
(c) => c.toJson(),
275222
);
276223

277224
// 4. Initialize Services.
278225
emailRepository = const HtEmailRepository(
279226
emailClient: HtEmailInMemoryClient(),
280227
);
281-
tokenBlacklistService = InMemoryTokenBlacklistService();
228+
tokenBlacklistService = InMemoryTokenBlacklistService(log: _log);
282229
authTokenService = JwtAuthTokenService(
283230
userRepository: userRepository,
284231
blacklistService: tokenBlacklistService,
285232
uuidGenerator: const Uuid(),
233+
log: _log,
286234
);
287235
verificationCodeStorageService = InMemoryVerificationCodeStorageService();
288236
authService = AuthService(
@@ -293,15 +241,17 @@ class AppDependencies {
293241
userAppSettingsRepository: userAppSettingsRepository,
294242
userContentPreferencesRepository: userContentPreferencesRepository,
295243
uuidGenerator: const Uuid(),
244+
log: _log,
296245
);
297246
dashboardSummaryService = DashboardSummaryService(
298247
headlineRepository: headlineRepository,
299-
categoryRepository: categoryRepository,
248+
topicRepository: topicRepository,
300249
sourceRepository: sourceRepository,
301250
);
302251
permissionService = const PermissionService();
303252
userPreferenceLimitService = DefaultUserPreferenceLimitService(
304-
appConfigRepository: appConfigRepository,
253+
remoteConfigRepository: remoteConfigRepository,
254+
log: _log,
305255
);
306256
}
307257

@@ -321,4 +271,20 @@ class AppDependencies {
321271
),
322272
);
323273
}
274+
275+
/// Converts DateTime values in a JSON map to ISO 8601 strings.
276+
///
277+
/// The postgres driver returns DateTime objects for TIMESTAMPTZ columns,
278+
/// but our models' `fromJson` factories expect ISO 8601 strings. This
279+
/// utility function performs the conversion for known timestamp fields.
280+
Map<String, dynamic> _convertTimestampsToString(Map<String, dynamic> json) {
281+
const timestampKeys = {'created_at', 'updated_at'};
282+
final newJson = Map<String, dynamic>.from(json);
283+
for (final key in timestampKeys) {
284+
if (newJson[key] is DateTime) {
285+
newJson[key] = (newJson[key] as DateTime).toIso8601String();
286+
}
287+
}
288+
return newJson;
289+
}
324290
}

0 commit comments

Comments
(0)

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